From 72db63f956e304f146fdf5f9db838c32d8e132bd Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Wed, 7 Jan 2026 09:17:45 +0700 Subject: [PATCH] Initial commit from monorepo --- Dockerfile | 24 + README.md | 3 + billing/__init__.py | 0 billing/asgi.py | 16 + billing/settings.py | 164 +++ billing/settings_local.py | 134 +++ billing/urls.py | 12 + billing/wsgi.py | 16 + billing_app/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 175 bytes billing_app/admin.py | 23 + billing_app/apps.py | 6 + billing_app/auth.py | 70 ++ billing_app/graphql_middleware.py | 74 ++ billing_app/migrations/0001_initial.py | 65 ++ ...2_remove_account_tigerbeetle_account_id.py | 17 + .../migrations/0003_refactor_models.py | 78 ++ billing_app/migrations/__init__.py | 0 billing_app/models.py | 78 ++ billing_app/permissions.py | 74 ++ billing_app/schemas/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 183 bytes .../__pycache__/m2m_schema.cpython-313.pyc | Bin 0 -> 8543 bytes .../__pycache__/team_schema.cpython-313.pyc | Bin 0 -> 7074 bytes billing_app/schemas/m2m_schema.py | 150 +++ billing_app/schemas/team_schema.py | 134 +++ .../billing_app/account/display_balances.html | 143 +++ billing_app/tests.py | 179 +++ billing_app/tigerbeetle_client.py | 146 +++ billing_app/views.py | 30 + create_accounts.py | 11 + docker-compose.yml | 43 + manage.py | 22 + nixpacks.toml | 18 + poetry.lock | 1016 +++++++++++++++++ pyproject.toml | 29 + test_tb_connection.py | 53 + transactions_script.py | 183 +++ 38 files changed, 3012 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 billing/__init__.py create mode 100644 billing/asgi.py create mode 100644 billing/settings.py create mode 100644 billing/settings_local.py create mode 100644 billing/urls.py create mode 100644 billing/wsgi.py create mode 100644 billing_app/__init__.py create mode 100644 billing_app/__pycache__/__init__.cpython-313.pyc create mode 100644 billing_app/admin.py create mode 100644 billing_app/apps.py create mode 100644 billing_app/auth.py create mode 100644 billing_app/graphql_middleware.py create mode 100644 billing_app/migrations/0001_initial.py create mode 100644 billing_app/migrations/0002_remove_account_tigerbeetle_account_id.py create mode 100644 billing_app/migrations/0003_refactor_models.py create mode 100644 billing_app/migrations/__init__.py create mode 100644 billing_app/models.py create mode 100644 billing_app/permissions.py create mode 100644 billing_app/schemas/__init__.py create mode 100644 billing_app/schemas/__pycache__/__init__.cpython-313.pyc create mode 100644 billing_app/schemas/__pycache__/m2m_schema.cpython-313.pyc create mode 100644 billing_app/schemas/__pycache__/team_schema.cpython-313.pyc create mode 100644 billing_app/schemas/m2m_schema.py create mode 100644 billing_app/schemas/team_schema.py create mode 100644 billing_app/templates/admin/billing_app/account/display_balances.html create mode 100644 billing_app/tests.py create mode 100644 billing_app/tigerbeetle_client.py create mode 100644 billing_app/views.py create mode 100644 create_accounts.py create mode 100644 docker-compose.yml create mode 100755 manage.py create mode 100644 nixpacks.toml create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 test_tb_connection.py create mode 100644 transactions_script.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..51e6696 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + NIXPACKS_POETRY_VERSION=2.2.1 + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential curl \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m venv --copies /opt/venv +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY . . + +RUN pip install --no-cache-dir poetry==$NIXPACKS_POETRY_VERSION \ + && poetry install --no-interaction --no-ansi + +ENV PORT=8000 + +CMD ["sh", "-c", "poetry run python manage.py migrate && poetry run python manage.py collectstatic --noinput && poetry run python -m gunicorn billing.wsgi:application --bind 0.0.0.0:${PORT:-8000}"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c4c759 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Billing Service + +This service is responsible for handling all billing-related logic, including integration with TigerBeetle for double-entry accounting. diff --git a/billing/__init__.py b/billing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/billing/asgi.py b/billing/asgi.py new file mode 100644 index 0000000..d34085b --- /dev/null +++ b/billing/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for billing project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'billing.settings') + +application = get_asgi_application() diff --git a/billing/settings.py b/billing/settings.py new file mode 100644 index 0000000..6dfa7b1 --- /dev/null +++ b/billing/settings.py @@ -0,0 +1,164 @@ +import os +from pathlib import Path +from urllib.parse import urlparse +from dotenv import load_dotenv +from infisical_sdk import InfisicalSDKClient +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + +load_dotenv() + +INFISICAL_API_URL = os.environ["INFISICAL_API_URL"] +INFISICAL_CLIENT_ID = os.environ["INFISICAL_CLIENT_ID"] +INFISICAL_CLIENT_SECRET = os.environ["INFISICAL_CLIENT_SECRET"] +INFISICAL_PROJECT_ID = os.environ["INFISICAL_PROJECT_ID"] +INFISICAL_ENV = os.environ.get("INFISICAL_ENV", "prod") + +client = InfisicalSDKClient(host=INFISICAL_API_URL) +client.auth.universal_auth.login( + client_id=INFISICAL_CLIENT_ID, + client_secret=INFISICAL_CLIENT_SECRET, +) + +# Fetch secrets from /billing and /shared +for secret_path in ["/billing", "/shared"]: + secrets_response = client.secrets.list_secrets( + environment_slug=INFISICAL_ENV, + secret_path=secret_path, + project_id=INFISICAL_PROJECT_ID, + expand_secret_references=True, + view_secret_value=True, + ) + for secret in secrets_response.secrets: + os.environ[secret.secretKey] = secret.secretValue + + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'dev-secret-key-change-in-production') + +DEBUG = os.getenv('DEBUG', 'False') == 'True' + +# Sentry/GlitchTip configuration +SENTRY_DSN = os.getenv('SENTRY_DSN', '') +if SENTRY_DSN: + sentry_sdk.init( + dsn=SENTRY_DSN, + integrations=[DjangoIntegration()], + auto_session_tracking=False, + traces_sample_rate=0.01, + release=os.getenv('RELEASE_VERSION', '1.0.0'), + environment=os.getenv('ENVIRONMENT', 'production'), + send_default_pii=False, + debug=DEBUG, + ) + +ALLOWED_HOSTS = ['*'] + +CSRF_TRUSTED_ORIGINS = ['https://billing.optovia.ru'] + +INSTALLED_APPS = [ + 'whitenoise.runserver_nostatic', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'graphene_django', + 'billing_app', # Added billing_app +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'billing.urls' # Changed to billing.urls + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'billing.wsgi.application' # Changed to billing.wsgi.application + +db_url = os.environ["BILLING_DATABASE_URL"] +parsed = urlparse(db_url) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': parsed.path.lstrip('/'), + 'USER': parsed.username, + 'PASSWORD': parsed.password, + 'HOST': parsed.hostname, + 'PORT': str(parsed.port) if parsed.port else '', + } +} + +# Internationalization +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# CORS +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = ['https://optovia.ru'] +CORS_ALLOW_CREDENTIALS = True + +# GraphQL +GRAPHENE = { + 'SCHEMA': 'billing_app.schema.schema', +} + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django.request': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + }, +} + +# Logto JWT settings +LOGTO_JWKS_URL = os.getenv('LOGTO_JWKS_URL', 'https://auth.optovia.ru/oidc/jwks') +LOGTO_ISSUER = os.getenv('LOGTO_ISSUER', 'https://auth.optovia.ru/oidc') +LOGTO_BILLING_AUDIENCE = os.getenv('LOGTO_BILLING_AUDIENCE', 'https://billing.optovia.ru') # Changed +# ID Token audience can be omitted when we only need signature + issuer validation. +LOGTO_ID_TOKEN_AUDIENCE = os.getenv('LOGTO_ID_TOKEN_AUDIENCE') diff --git a/billing/settings_local.py b/billing/settings_local.py new file mode 100644 index 0000000..8233bf5 --- /dev/null +++ b/billing/settings_local.py @@ -0,0 +1,134 @@ +import os +from pathlib import Path +from urllib.parse import urlparse +from dotenv import load_dotenv +import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration + +load_dotenv() + + + + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'dev-secret-key-change-in-production') + +DEBUG = True + +# Sentry/GlitchTip configuration +SENTRY_DSN = os.getenv('SENTRY_DSN', '') +if SENTRY_DSN: + sentry_sdk.init( + dsn=SENTRY_DSN, + integrations=[DjangoIntegration()], + auto_session_tracking=False, + traces_sample_rate=0.01, + release=os.getenv('RELEASE_VERSION', '1.0.0'), + environment=os.getenv('ENVIRONMENT', 'production'), + send_default_pii=False, + debug=DEBUG, + ) + +ALLOWED_HOSTS = ['*'] + +CSRF_TRUSTED_ORIGINS = ['https://billing.optovia.ru'] + +INSTALLED_APPS = [ + 'whitenoise.runserver_nostatic', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'corsheaders', + 'graphene_django', + 'billing_app', # Added billing_app +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'billing.urls' # Changed to billing.urls + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'billing.wsgi.application' # Changed to billing.wsgi.application + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} +# Internationalization +LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +# Static files +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# CORS +CORS_ALLOW_ALL_ORIGINS = False +CORS_ALLOWED_ORIGINS = ['http://localhost:3000', 'https://optovia.ru'] +CORS_ALLOW_CREDENTIALS = True + +# GraphQL +GRAPHENE = { + 'SCHEMA': 'billing_app.schema.schema', +} + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'django.request': { + 'handlers': ['console'], + 'level': 'DEBUG', + 'propagate': False, + }, + }, +} + +# Logto JWT settings +LOGTO_JWKS_URL = os.getenv('LOGTO_JWKS_URL', 'https://auth.optovia.ru/oidc/jwks') +LOGTO_ISSUER = os.getenv('LOGTO_ISSUER', 'https://auth.optovia.ru/oidc') +LOGTO_BILLING_AUDIENCE = os.getenv('LOGTO_BILLING_AUDIENCE', 'https://billing.optovia.ru') # Changed +# ID Token audience can be omitted when we only need signature + issuer validation. +LOGTO_ID_TOKEN_AUDIENCE = os.getenv('LOGTO_ID_TOKEN_AUDIENCE') diff --git a/billing/urls.py b/billing/urls.py new file mode 100644 index 0000000..9b924f1 --- /dev/null +++ b/billing/urls.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from billing_app.views import PublicGraphQLView, M2MGraphQLView, TeamGraphQLView +from billing_app.schemas.m2m_schema import m2m_schema +from billing_app.schemas.team_schema import team_schema + +urlpatterns = [ + path('admin/', admin.site.urls), + path('graphql/m2m/', csrf_exempt(M2MGraphQLView.as_view(graphiql=False, schema=m2m_schema))), + path('graphql/team/', csrf_exempt(TeamGraphQLView.as_view(graphiql=True, schema=team_schema))), +] diff --git a/billing/wsgi.py b/billing/wsgi.py new file mode 100644 index 0000000..377f777 --- /dev/null +++ b/billing/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for billing project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'billing.settings') + +application = get_wsgi_application() diff --git a/billing_app/__init__.py b/billing_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/billing_app/__pycache__/__init__.cpython-313.pyc b/billing_app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66073748fa528408ca3a6f0a7557f91590f4a386 GIT binary patch literal 175 zcmey&%ge<81b&ySGC}lX5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~iKerR!OQL%ne zX>m?sUQ%LqW@?#!d45rLaY15os(yY!Nq$*oqJC0ha&~H7O0j-YW=>9KUOJ49Pb?_V skB`sH%PfhH*DI*J#bJ}1pHiBWYFESxG#+GqF^KVznURsPh#ANN0C})2<^TWy literal 0 HcmV?d00001 diff --git a/billing_app/admin.py b/billing_app/admin.py new file mode 100644 index 0000000..5891edd --- /dev/null +++ b/billing_app/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin +from .models import Account, OperationCode, ServiceAccount + + +@admin.register(Account) +class AccountAdmin(admin.ModelAdmin): + list_display = ('uuid', 'name', 'account_type', 'created_at') + list_filter = ('account_type', 'created_at') + search_fields = ('uuid', 'name') + readonly_fields = ('uuid', 'created_at') + + +@admin.register(OperationCode) +class OperationCodeAdmin(admin.ModelAdmin): + list_display = ('code', 'name', 'created_at') + search_fields = ('code', 'name') + ordering = ('code',) + + +@admin.register(ServiceAccount) +class ServiceAccountAdmin(admin.ModelAdmin): + list_display = ('slug', 'account') + search_fields = ('slug', 'account__name') diff --git a/billing_app/apps.py b/billing_app/apps.py new file mode 100644 index 0000000..4747e3a --- /dev/null +++ b/billing_app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BillingAppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'billing_app' diff --git a/billing_app/auth.py b/billing_app/auth.py new file mode 100644 index 0000000..986b60d --- /dev/null +++ b/billing_app/auth.py @@ -0,0 +1,70 @@ +import logging +from typing import Iterable, Optional + +import jwt +from django.conf import settings +from jwt import InvalidTokenError, PyJWKClient + + +logger = logging.getLogger(__name__) + + +class LogtoTokenValidator: + """Validate JWTs issued by Logto using the published JWKS.""" + + def __init__(self, jwks_url: str, issuer: str): + self._issuer = issuer + self._jwks_client = PyJWKClient(jwks_url) + + def decode( + self, + token: str, + audience: Optional[str] = None, + ) -> dict: + """Decode and verify a JWT, enforcing issuer and optional audience.""" + try: + signing_key = self._jwks_client.get_signing_key_from_jwt(token) + header_alg = jwt.get_unverified_header(token).get("alg") + + return jwt.decode( + token, + signing_key.key, + algorithms=[header_alg] if header_alg else None, + issuer=self._issuer, + audience=audience, + options={"verify_aud": audience is not None}, + ) + except InvalidTokenError as exc: + logger.warning("Failed to validate Logto token: %s", exc) + raise + + +def get_bearer_token(request) -> str: + """Extract Bearer token from Authorization header.""" + auth_header = request.META.get("HTTP_AUTHORIZATION", "") + if not auth_header.startswith("Bearer "): + raise InvalidTokenError("Missing Bearer token") + + token = auth_header.split(" ", 1)[1] + if not token or token == "undefined": + raise InvalidTokenError("Empty Bearer token") + + return token + + +def scopes_from_payload(payload: dict) -> list[str]: + """Split scope string (if present) into a list.""" + scope_value = payload.get("scope") + if not scope_value: + return [] + if isinstance(scope_value, str): + return scope_value.split() + if isinstance(scope_value, Iterable): + return list(scope_value) + return [] + + +validator = LogtoTokenValidator( + getattr(settings, "LOGTO_JWKS_URL", "https://auth.optovia.ru/oidc/jwks"), + getattr(settings, "LOGTO_ISSUER", "https://auth.optovia.ru/oidc"), +) diff --git a/billing_app/graphql_middleware.py b/billing_app/graphql_middleware.py new file mode 100644 index 0000000..2b95bad --- /dev/null +++ b/billing_app/graphql_middleware.py @@ -0,0 +1,74 @@ +""" +GraphQL middleware for JWT authentication and scope checking in the Billing service. + +This middleware runs for every GraphQL resolver and sets user_id and scopes +on info.context based on the JWT token in Authorization header. +""" +import logging +from django.conf import settings +from graphql import GraphQLError +from jwt import InvalidTokenError + +from .auth import get_bearer_token, scopes_from_payload, validator + + +def _is_introspection(info) -> bool: + """Возвращает True для любых introspection резолвов.""" + field = getattr(info, "field_name", "") + parent = getattr(getattr(info, "parent_type", None), "name", "") + return field.startswith("__") or parent.startswith("__") + + +class PublicNoAuthMiddleware: + """Public endpoint - no authentication required.""" + + def resolve(self, next, root, info, **kwargs): + return next(root, info, **kwargs) + + +class M2MNoAuthMiddleware: + """M2M endpoint - internal services only, no auth for now.""" + + def resolve(self, next, root, info, **kwargs): + return next(root, info, **kwargs) + + +class BillingJWTMiddleware: + """ + Middleware for Billing service operations. + Sets info.context.user_id and scopes from the JWT access token. + """ + + def resolve(self, next, root, info, **kwargs): + request = info.context + if _is_introspection(info): + return next(root, info, **kwargs) + + # Skip if JWT processing already happened (e.g., another middleware already set it) + if hasattr(request, '_billing_jwt_processed') and request._billing_jwt_processed: + return next(root, info, **kwargs) + + request._billing_jwt_processed = True + + try: + token = get_bearer_token(request) + payload = validator.decode( + token, + audience=getattr(settings, 'LOGTO_BILLING_AUDIENCE', None), + ) + request.user_id = payload.get('sub') # Subject (user ID) + request.team_uuid = payload.get('team_uuid') + request.scopes = scopes_from_payload(payload) + if not request.team_uuid or 'teams:member' not in request.scopes: + raise GraphQLError("Unauthorized") + + logging.debug(f"JWT processed for user_id: {request.user_id}, scopes: {request.scopes}") + + except InvalidTokenError as exc: + logging.info(f"Billing JWT authentication failed: {exc}") + raise GraphQLError("Unauthorized") from exc + except Exception as exc: + logging.error(f"An unexpected error occurred during JWT processing: {exc}") + raise GraphQLError("Unauthorized") from exc + + return next(root, info, **kwargs) diff --git a/billing_app/migrations/0001_initial.py b/billing_app/migrations/0001_initial.py new file mode 100644 index 0000000..079356c --- /dev/null +++ b/billing_app/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.9 on 2025-12-10 11:19 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(help_text='Name or identifier for the account', max_length=255)), + ('account_type', models.CharField(choices=[('USER', 'User Account'), ('SERVICE', 'Service Account'), ('ASSET', 'Asset Account'), ('LIABILITY', 'Liability Account'), ('REVENUE', 'Revenue Account'), ('EXPENSE', 'Expense Account')], default='USER', help_text='Type of the account (e.g., User, Service, Revenue)', max_length=50)), + ('description', models.TextField(blank=True, help_text='Detailed description of the account', null=True)), + ('tigerbeetle_account_id', models.BigIntegerField(help_text='The unique ID of this account in TigerBeetle', unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Billing Account', + 'verbose_name_plural': 'Billing Accounts', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='TransactionReason', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Unique name for the transaction reason', max_length=255, unique=True)), + ('description', models.TextField(blank=True, help_text='Detailed description of this transaction reason', null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Transaction Reason', + 'verbose_name_plural': 'Transaction Reasons', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Operation', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('amount', models.DecimalField(decimal_places=4, help_text='The amount transferred in this operation', max_digits=20)), + ('state', models.CharField(choices=[('PENDING', 'Pending TigerBeetle transfer'), ('COMPLETED', 'TigerBeetle transfer completed'), ('FAILED', 'TigerBeetle transfer failed')], default='PENDING', help_text='Current state of the TigerBeetle transfer for this operation', max_length=50)), + ('tigerbeetle_transfer_id', models.BigIntegerField(blank=True, help_text='The unique ID of the transfer in TigerBeetle, set after successful transfer', null=True, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('destination_account', models.ForeignKey(help_text='The account to which funds are moved', on_delete=django.db.models.deletion.PROTECT, related_name='incoming_operations', to='billing_app.account')), + ('source_account', models.ForeignKey(help_text='The account from which funds are moved', on_delete=django.db.models.deletion.PROTECT, related_name='outgoing_operations', to='billing_app.account')), + ('reason', models.ForeignKey(help_text='The business reason for this operation', on_delete=django.db.models.deletion.PROTECT, related_name='operations', to='billing_app.transactionreason')), + ], + options={ + 'verbose_name': 'Business Operation', + 'verbose_name_plural': 'Business Operations', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/billing_app/migrations/0002_remove_account_tigerbeetle_account_id.py b/billing_app/migrations/0002_remove_account_tigerbeetle_account_id.py new file mode 100644 index 0000000..e7c3cb0 --- /dev/null +++ b/billing_app/migrations/0002_remove_account_tigerbeetle_account_id.py @@ -0,0 +1,17 @@ +# Generated manually + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('billing_app', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='account', + name='tigerbeetle_account_id', + ), + ] diff --git a/billing_app/migrations/0003_refactor_models.py b/billing_app/migrations/0003_refactor_models.py new file mode 100644 index 0000000..98d88a3 --- /dev/null +++ b/billing_app/migrations/0003_refactor_models.py @@ -0,0 +1,78 @@ +# Generated manually - refactor billing models + +import django.db.models.deletion +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('billing_app', '0002_remove_account_tigerbeetle_account_id'), + ] + + operations = [ + # 1. Delete old models + migrations.DeleteModel( + name='Operation', + ), + migrations.DeleteModel( + name='TransactionReason', + ), + + # 2. Alter Account model - update account_type choices and remove name help_text + migrations.AlterField( + model_name='account', + name='account_type', + field=models.CharField( + choices=[('USER', 'User Account'), ('SERVICE', 'Service Account')], + default='USER', + max_length=50, + ), + ), + migrations.AlterField( + model_name='account', + name='name', + field=models.CharField(help_text='Название аккаунта', max_length=255), + ), + migrations.AlterModelOptions( + name='account', + options={'ordering': ['-created_at'], 'verbose_name': 'Account', 'verbose_name_plural': 'Accounts'}, + ), + + # 3. Create OperationCode model + migrations.CreateModel( + name='OperationCode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.PositiveIntegerField(help_text='Числовой код для TigerBeetle', unique=True)), + ('name', models.CharField(help_text='Название операции', max_length=255, unique=True)), + ('description', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Operation Code', + 'verbose_name_plural': 'Operation Codes', + 'ordering': ['code'], + }, + ), + + # 4. Create ServiceAccount model + migrations.CreateModel( + name='ServiceAccount', + fields=[ + ('account', models.OneToOneField( + on_delete=models.deletion.CASCADE, + primary_key=True, + related_name='service_info', + serialize=False, + to='billing_app.account' + )), + ('slug', models.SlugField(help_text='Уникальный идентификатор (bank, revenue, etc)', unique=True)), + ], + options={ + 'verbose_name': 'Service Account', + 'verbose_name_plural': 'Service Accounts', + }, + ), + ] diff --git a/billing_app/migrations/__init__.py b/billing_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/billing_app/models.py b/billing_app/models.py new file mode 100644 index 0000000..ffd39df --- /dev/null +++ b/billing_app/models.py @@ -0,0 +1,78 @@ +import uuid +from django.db import models + + +class AccountType(models.TextChoices): + """Типы аккаунтов в системе биллинга.""" + USER = 'USER', 'User Account' + SERVICE = 'SERVICE', 'Service Account' + + +class Account(models.Model): + """ + Аккаунт в системе биллинга. + Хранит метаданные об аккаунтах, созданных в TigerBeetle. + UUID используется как ID аккаунта в TigerBeetle (uuid.int). + """ + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=255, help_text="Название аккаунта") + account_type = models.CharField( + max_length=50, + choices=AccountType.choices, + default=AccountType.USER, + ) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + @property + def tigerbeetle_id(self): + """ID аккаунта в TigerBeetle (128-bit int из UUID).""" + return self.uuid.int + + def __str__(self): + return f"{self.name} ({self.account_type})" + + class Meta: + verbose_name = "Account" + verbose_name_plural = "Accounts" + ordering = ['-created_at'] + + +class OperationCode(models.Model): + """ + Код операции (справочник типов транзакций). + Используется как 'code' поле в TigerBeetle transfer. + """ + code = models.PositiveIntegerField(unique=True, help_text="Числовой код для TigerBeetle") + name = models.CharField(max_length=255, unique=True, help_text="Название операции") + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.code}: {self.name}" + + class Meta: + verbose_name = "Operation Code" + verbose_name_plural = "Operation Codes" + ordering = ['code'] + + +class ServiceAccount(models.Model): + """ + Сервисный аккаунт (банк, revenue, etc). + Это Account с особым назначением в системе. + """ + account = models.OneToOneField( + Account, + on_delete=models.CASCADE, + primary_key=True, + related_name='service_info' + ) + slug = models.SlugField(unique=True, help_text="Уникальный идентификатор (bank, revenue, etc)") + + def __str__(self): + return f"{self.slug}: {self.account.name}" + + class Meta: + verbose_name = "Service Account" + verbose_name_plural = "Service Accounts" diff --git a/billing_app/permissions.py b/billing_app/permissions.py new file mode 100644 index 0000000..d44f02d --- /dev/null +++ b/billing_app/permissions.py @@ -0,0 +1,74 @@ +""" +Декоратор для проверки scopes в JWT токене. +Используется для защиты GraphQL резолверов. +""" +from functools import wraps +from graphql import GraphQLError + + +def require_scopes(*scopes: str): + """ + Декоратор для проверки наличия scopes в JWT токене. + + Использование: + @require_scopes("read:teams") + def resolve_team(self, info): + ... + + @require_scopes("read:teams", "write:teams") + def resolve_update_team(self, info): + ... + """ + def decorator(func): + # Сохраняем scopes в метаданных для возможности сбора всех scopes + if not hasattr(func, '_required_scopes'): + func._required_scopes = [] + func._required_scopes.extend(scopes) + + @wraps(func) + def wrapper(self, info, *args, **kwargs): + # Получаем scopes из контекста (должны быть добавлены в middleware) + user_scopes = set(getattr(info.context, 'scopes', []) or []) + + missing = set(scopes) - user_scopes + if missing: + raise GraphQLError(f"Missing required scopes: {', '.join(missing)}") + + return func(self, info, *args, **kwargs) + + # Переносим метаданные на wrapper + wrapper._required_scopes = func._required_scopes + return wrapper + return decorator + + +def collect_scopes_from_schema(schema) -> set: + """ + Собирает все scopes из схемы для синхронизации с Logto. + + Использование: + from .schema import schema + scopes = collect_scopes_from_schema(schema) + # {'read:team', 'invite:member', ...} + """ + scopes = set() + + # Query resolvers + if hasattr(schema, 'query') and schema.query: + query_type = schema.query + for field_name in dir(query_type): + if field_name.startswith('resolve_'): + resolver = getattr(query_type, field_name, None) + if resolver and hasattr(resolver, '_required_scopes'): + scopes.update(resolver._required_scopes) + + # Mutation resolvers + if hasattr(schema, 'mutation') and schema.mutation: + mutation_type = schema.mutation + for field_name, field in mutation_type._meta.fields.items(): + if hasattr(field, 'type') and hasattr(field.type, 'mutate'): + mutate = field.type.mutate + if hasattr(mutate, '_required_scopes'): + scopes.update(mutate._required_scopes) + + return scopes diff --git a/billing_app/schemas/__init__.py b/billing_app/schemas/__init__.py new file mode 100644 index 0000000..b042308 --- /dev/null +++ b/billing_app/schemas/__init__.py @@ -0,0 +1 @@ +# Billing schemas diff --git a/billing_app/schemas/__pycache__/__init__.cpython-313.pyc b/billing_app/schemas/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..364bdfa1e7f86bfbff45f4391998e160941e66fb GIT binary patch literal 183 zcmey&%ge<81hY3sWC{W4#~=<2FhUuhS%8eG4CxG-jD9N_ikN`B&mgH=VfvxPsYS*5 zMWw|#iFrwh*_o+j`sMjW*~JBk$*KDJ1ts}qnTh&IiOJcic`3#ENtro0nR)3jIzF+W zK)*OSBQ-a%SU)~KGcU6wK3=b&@)n0pZhlH>PO4oIE6^H{Gm1fskIamWj77{q765T{ BF<$@x literal 0 HcmV?d00001 diff --git a/billing_app/schemas/__pycache__/m2m_schema.cpython-313.pyc b/billing_app/schemas/__pycache__/m2m_schema.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..774ed7e7be6865fcb097f738cc5abfdcbd5b7053 GIT binary patch literal 8543 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 iUDtYOZz;5Q3sP5TZY {to_uuid}, amount={input.amount}") + return CreateTransaction( + success=True, + message="Transaction completed", + transferId=str(transfer_id) + ) + + +class M2MQuery(graphene.ObjectType): + operationCodes = graphene.List(OperationCodeType, description="List of operation codes") + serviceAccounts = graphene.List(ServiceAccountType, description="List of service accounts (bank, revenue)") + + def resolve_operationCodes(self, info): + return OperationCodeModel.objects.all() + + def resolve_serviceAccounts(self, info): + return ServiceAccountModel.objects.select_related('account').all() + + +class M2MMutation(graphene.ObjectType): + createTransaction = CreateTransaction.Field() + + +m2m_schema = graphene.Schema(query=M2MQuery, mutation=M2MMutation) diff --git a/billing_app/schemas/team_schema.py b/billing_app/schemas/team_schema.py new file mode 100644 index 0000000..bbc5472 --- /dev/null +++ b/billing_app/schemas/team_schema.py @@ -0,0 +1,134 @@ +""" +Team-level GraphQL schema for Billing service. +Requires teams:member scope (verified by middleware). +Provides teamBalance and teamTransactions for the authenticated team. + +All data comes directly from TigerBeetle. +""" +import graphene +import uuid +import logging +from graphql import GraphQLError + +from ..tigerbeetle_client import tigerbeetle_client +from ..permissions import require_scopes +from ..models import OperationCode + +logger = logging.getLogger(__name__) + + +class TeamBalance(graphene.ObjectType): + """Balance information for a team's account from TigerBeetle.""" + balance = graphene.Float(required=True, description="Current balance (credits - debits)") + creditsPosted = graphene.Float(required=True, description="Total credits posted") + debitsPosted = graphene.Float(required=True, description="Total debits posted") + exists = graphene.Boolean(required=True, description="Whether account exists in TigerBeetle") + + +class TeamTransaction(graphene.ObjectType): + """Transaction from TigerBeetle.""" + id = graphene.String(required=True) + amount = graphene.Float(required=True) + timestamp = graphene.Float(description="TigerBeetle timestamp") + code = graphene.Int(description="Operation code") + codeName = graphene.String(description="Operation code name from OperationCode table") + direction = graphene.String(required=True, description="'credit' or 'debit' relative to team account") + counterpartyUuid = graphene.String(description="UUID of the other account in transaction") + + +class TeamQuery(graphene.ObjectType): + teamBalance = graphene.Field(TeamBalance, description="Get balance for the authenticated team") + teamTransactions = graphene.List( + TeamTransaction, + limit=graphene.Int(default_value=50), + description="Get transactions for the authenticated team" + ) + + @require_scopes("teams:member") + def resolve_teamBalance(self, info): + team_uuid = getattr(info.context, 'team_uuid', None) + if not team_uuid: + raise GraphQLError("Team UUID not found in context") + + try: + team_uuid_obj = uuid.UUID(team_uuid) + tb_account_id = team_uuid_obj.int + + # Look up account in TigerBeetle + accounts = tigerbeetle_client.lookup_accounts([tb_account_id]) + + if not accounts: + # Account doesn't exist yet - return zero balance + return TeamBalance( + balance=0.0, + creditsPosted=0.0, + debitsPosted=0.0, + exists=False + ) + + account = accounts[0] + credits_posted = float(account.credits_posted) + debits_posted = float(account.debits_posted) + balance = credits_posted - debits_posted + + return TeamBalance( + balance=balance, + creditsPosted=credits_posted, + debitsPosted=debits_posted, + exists=True + ) + + except ValueError as e: + logger.error(f"Invalid team UUID format: {team_uuid} - {e}") + raise GraphQLError("Invalid team UUID format") + except Exception as e: + logger.error(f"Error fetching team balance: {e}") + raise GraphQLError("Failed to fetch team balance") + + @require_scopes("teams:member") + def resolve_teamTransactions(self, info, limit=50): + team_uuid = getattr(info.context, 'team_uuid', None) + if not team_uuid: + raise GraphQLError("Team UUID not found in context") + + try: + team_uuid_obj = uuid.UUID(team_uuid) + tb_account_id = team_uuid_obj.int + + # Get transfers from TigerBeetle + transfers = tigerbeetle_client.get_account_transfers(tb_account_id, limit=limit) + + # Load operation codes for name lookup + code_map = {oc.code: oc.name for oc in OperationCode.objects.all()} + + result = [] + for t in transfers: + # Determine direction relative to team account + if t.credit_account_id == tb_account_id: + direction = "credit" + counterparty_id = t.debit_account_id + else: + direction = "debit" + counterparty_id = t.credit_account_id + + result.append(TeamTransaction( + id=str(uuid.UUID(int=t.id)), + amount=float(t.amount), + timestamp=float(t.timestamp), + code=t.code, + codeName=code_map.get(t.code), + direction=direction, + counterpartyUuid=str(uuid.UUID(int=counterparty_id)), + )) + + return result + + except ValueError as e: + logger.error(f"Invalid team UUID format: {team_uuid} - {e}") + raise GraphQLError("Invalid team UUID format") + except Exception as e: + logger.error(f"Error fetching team transactions: {e}") + raise GraphQLError("Failed to fetch team transactions") + + +team_schema = graphene.Schema(query=TeamQuery) diff --git a/billing_app/templates/admin/billing_app/account/display_balances.html b/billing_app/templates/admin/billing_app/account/display_balances.html new file mode 100644 index 0000000..6889928 --- /dev/null +++ b/billing_app/templates/admin/billing_app/account/display_balances.html @@ -0,0 +1,143 @@ +{% extends 'admin/base.html' %} +{% load i18n admin_urls static admin_list %} + +{% block extrahead %} + {{ block.super }} + {{ media.css }} + {{ media.js }} + +{% endblock %} + +{% block content %} +
+

{{ title }}

+ + {% if error_message %} +

{{ error_message }}

+ {% endif %} + + {% if accounts %} +
+

{% translate "Account Details" %}

+ + + + + + + + + + + + + + + + + + + + {% for account in accounts %} + + + + + + + + + + + + + + + + {% empty %} + + {% endfor %} + +
{% translate "Account UUID" %}{% translate "Net Balance" %}{% translate "Debits Posted" %}{% translate "Credits Posted" %}{% translate "Debits Pending" %}{% translate "Credits Pending" %}{% translate "Ledger" %}{% translate "Code" %}{% translate "Flags" %}{% translate "Timestamp" %}{% translate "User Data 128" %}{% translate "User Data 64" %}{% translate "User Data 32" %}
{{ account.uuid }}{{ account.net_balance }}{{ account.debits_posted }}{{ account.credits_posted }}{{ account.debits_pending }}{{ account.credits_pending }}{{ account.ledger }}{{ account.code }}{{ account.flags }}{{ account.timestamp }}{{ account.user_data_128 }}{{ account.user_data_64 }}{{ account.user_data_32 }}
{% translate "No account details to display." %}
+
+ {% else %} +

{% translate "No account details to display." %}

+ {% endif %} + + {% if transfers %} +
+

{% translate "Transfer Details" %}

+ + + + + + + + + + + + + + + + + + + + {% for transfer in transfers %} + + + + + + + + + + + + + + + + {% empty %} + + {% endfor %} + +
{% translate "Transfer ID" %}{% translate "Debit Account" %}{% translate "Credit Account" %}{% translate "Amount" %}{% translate "Ledger" %}{% translate "Code" %}{% translate "Flags" %}{% translate "Timestamp" %}{% translate "Pending ID" %}{% translate "Timeout" %}{% translate "User Data 128" %}{% translate "User Data 64" %}{% translate "User Data 32" %}
{{ transfer.id }}{{ transfer.debit_account_id }}{{ transfer.credit_account_id }}{{ transfer.amount }}{{ transfer.ledger }}{{ transfer.code }}{{ transfer.flags }}{{ transfer.timestamp }}{{ transfer.pending_id }}{{ transfer.timeout }}{{ transfer.user_data_128 }}{{ transfer.user_data_64 }}{{ transfer.user_data_32 }}
{% translate "No transfer details to display." %}
+
+ {% else %} +

{% translate "No transfer details to display." %}

+ {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/billing_app/tests.py b/billing_app/tests.py new file mode 100644 index 0000000..89c9401 --- /dev/null +++ b/billing_app/tests.py @@ -0,0 +1,179 @@ +import json +from unittest.mock import patch, MagicMock +from graphene_django.utils.testing import GraphQLTestCase +from .models import Account +import uuid + +class BillingGraphQLTests(GraphQLTestCase): + GRAPHQL_URL = '/graphql/public/' + + @patch('billing_app.schemas.tigerbeetle_client') + def test_create_transfer_new_accounts(self, mock_tb_client): + """ + Tests that a transfer is created successfully and that new accounts + are created lazily in both the local DB and in TigerBeetle. + """ + # Configure the mock to return no errors + mock_tb_client.create_accounts.return_value = [] + mock_tb_client.create_transfers.return_value = [] + + debit_account_id = str(uuid.uuid4()) + credit_account_id = str(uuid.uuid4()) + amount = 100 + ledger = 1 + code = 200 + + # Ensure accounts do not exist locally + self.assertEqual(Account.objects.count(), 0) + + response = self.query( + ''' + mutation createTransfer( + $debitAccountId: String!, + $creditAccountId: String!, + $amount: Int!, + $ledger: Int!, + $code: Int! + ) { + createTransfer( + debitAccountId: $debitAccountId, + creditAccountId: $creditAccountId, + amount: $amount, + ledger: $ledger, + code: $code + ) { + success + message + transferId + } + } + ''', + variables={ + 'debitAccountId': debit_account_id, + 'creditAccountId': credit_account_id, + 'amount': amount, + 'ledger': ledger, + 'code': code + } + ) + + self.assertResponseNoErrors(response) + content = json.loads(response.content) + result = content['data']['createTransfer'] + + self.assertTrue(result['success']) + self.assertIsNotNone(result['transferId']) + self.assertEqual(result['message'], 'Transfer completed successfully.') + + # Verify local accounts were created + self.assertEqual(Account.objects.count(), 2) + self.assertTrue(Account.objects.filter(uuid=debit_account_id).exists()) + self.assertTrue(Account.objects.filter(uuid=credit_account_id).exists()) + + # Verify TigerBeetle client was called correctly + mock_tb_client.create_accounts.assert_called_once() + self.assertEqual(len(mock_tb_client.create_accounts.call_args[0][0]), 2) # Called with 2 accounts + + mock_tb_client.create_transfers.assert_called_once() + self.assertEqual(len(mock_tb_client.create_transfers.call_args[0][0]), 1) # Called with 1 transfer + transfer_arg = mock_tb_client.create_transfers.call_args[0][0][0] + self.assertEqual(transfer_arg.amount, amount) + + @patch('billing_app.schemas.tigerbeetle_client') + def test_create_transfer_existing_accounts(self, mock_tb_client): + """ + Tests that a transfer is created successfully with existing accounts + and that `create_accounts` is not called. + """ + mock_tb_client.create_transfers.return_value = [] + + debit_account_id = uuid.uuid4() + credit_account_id = uuid.uuid4() + + # Pre-populate the local database + Account.objects.create(uuid=debit_account_id) + Account.objects.create(uuid=credit_account_id) + + self.assertEqual(Account.objects.count(), 2) + + response = self.query( + ''' + mutation createTransfer( + $debitAccountId: String!, + $creditAccountId: String!, + $amount: Int!, + $ledger: Int!, + $code: Int! + ) { + createTransfer( + debitAccountId: $debitAccountId, + creditAccountId: $creditAccountId, + amount: $amount, + ledger: $ledger, + code: $code + ) { + success + message + transferId + } + } + ''', + variables={ + 'debitAccountId': str(debit_account_id), + 'creditAccountId': str(credit_account_id), + 'amount': 50, + 'ledger': 1, + 'code': 201 + } + ) + + self.assertResponseNoErrors(response) + + # Verify that create_accounts was NOT called + mock_tb_client.create_accounts.assert_not_called() + + # Verify that create_transfers WAS called + mock_tb_client.create_transfers.assert_called_once() + + def test_create_transfer_invalid_uuid(self): + """ + Tests that the mutation fails gracefully with an invalid UUID. + """ + response = self.query( + ''' + mutation createTransfer( + $debitAccountId: String!, + $creditAccountId: String!, + $amount: Int!, + $ledger: Int!, + $code: Int! + ) { + createTransfer( + debitAccountId: $debitAccountId, + creditAccountId: $creditAccountId, + amount: $amount, + ledger: $ledger, + code: $code + ) { + success + message + transferId + } + } + ''', + variables={ + 'debitAccountId': 'not-a-uuid', + 'creditAccountId': str(uuid.uuid4()), + 'amount': 50, + 'ledger': 1, + 'code': 202 + } + ) + + self.assertResponseNoErrors(response) + content = json.loads(response.content) + result = content['data']['createTransfer'] + + self.assertFalse(result['success']) + self.assertEqual(result['message'], 'Invalid account ID format. Must be a valid UUID.') + diff --git a/billing_app/tigerbeetle_client.py b/billing_app/tigerbeetle_client.py new file mode 100644 index 0000000..8c7bf68 --- /dev/null +++ b/billing_app/tigerbeetle_client.py @@ -0,0 +1,146 @@ +import os +import logging +import socket + +logger = logging.getLogger(__name__) + + +def resolve_address(address: str) -> str: + """Resolve hostname to IP address for TigerBeetle client. + + TigerBeetle Python client doesn't handle DNS resolution properly, + so we need to resolve hostnames to IPs before connecting. + """ + if ":" in address: + host, port = address.rsplit(":", 1) + else: + host, port = address, "3000" + + try: + ip = socket.gethostbyname(host) + resolved = f"{ip}:{port}" + logger.info(f"Resolved {address} to {resolved}") + return resolved + except socket.gaierror as e: + logger.warning(f"Failed to resolve {host}: {e}, using original address") + return address + + +class TigerBeetleClient: + """ + Lazy-initialized TigerBeetle client singleton. + Connection is only established on first actual use. + + IMPORTANT: TigerBeetle Python client requires io_uring. + Docker container must run with: --security-opt seccomp=unconfined + """ + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super(TigerBeetleClient, cls).__new__(cls) + cls._instance._client = None + cls._instance._initialized = False + return cls._instance + + def _ensure_connected(self): + """Lazy initialization - connect only when needed.""" + if self._initialized: + return self._client is not None + + self._initialized = True + self.cluster_id = int(os.getenv("TB_CLUSTER_ID", "0")) + raw_address = os.getenv("TB_ADDRESS", "127.0.0.1:3000") + self.replica_addresses = resolve_address(raw_address) + + try: + import tigerbeetle as tb + self._client = tb.ClientSync( + cluster_id=self.cluster_id, replica_addresses=self.replica_addresses + ) + logger.info(f"Connected to TigerBeetle cluster {self.cluster_id} at {self.replica_addresses}") + return True + except Exception as e: + logger.error(f"Failed to connect to TigerBeetle: {e}") + self._client = None + return False + + def close(self): + if self._client: + self._client.close() + logger.info("TigerBeetle client closed.") + + def create_accounts(self, accounts): + """Create accounts in TigerBeetle.""" + if not self._ensure_connected(): + logger.error("TigerBeetle client not available.") + return [Exception("TigerBeetle not connected")] + + try: + errors = self._client.create_accounts(accounts) + if errors: + for error in errors: + if hasattr(error, 'error'): + logger.error(f"Error creating account: {error.error.name}") + else: + logger.error(f"Error creating account: {error}") + return errors + except Exception as e: + logger.error(f"Exception during account creation: {e}") + return [e] + + def create_transfers(self, transfers): + """Create transfers in TigerBeetle.""" + if not self._ensure_connected(): + logger.error("TigerBeetle client not available.") + return [Exception("TigerBeetle not connected")] + + try: + errors = self._client.create_transfers(transfers) + if errors: + for error in errors: + if hasattr(error, 'error'): + logger.error(f"Error creating transfer: {error.error.name}") + else: + logger.error(f"Error creating transfer: {error}") + return errors + except Exception as e: + logger.error(f"Exception during transfer creation: {e}") + return [e] + + def lookup_accounts(self, account_ids: list[int]): + """Look up accounts in TigerBeetle.""" + if not self._ensure_connected(): + logger.error("TigerBeetle client not available.") + return [] + try: + accounts = self._client.lookup_accounts(account_ids) + return accounts + except Exception as e: + logger.error(f"Exception during account lookup: {e}") + return [] + + def get_account_transfers(self, account_id: int, limit: int = 50): + """Get transfers for an account from TigerBeetle.""" + if not self._ensure_connected(): + logger.error("TigerBeetle client not available.") + return [] + try: + import tigerbeetle as tb + account_filter = tb.AccountFilter( + account_id=account_id, + timestamp_min=0, + timestamp_max=0, + limit=limit, + flags=tb.AccountFilterFlags.CREDITS | tb.AccountFilterFlags.DEBITS | tb.AccountFilterFlags.REVERSED, + ) + transfers = self._client.get_account_transfers(account_filter) + return transfers + except Exception as e: + logger.error(f"Exception during get_account_transfers: {e}") + return [] + + +# Singleton instance (lazy - doesn't connect until first use) +tigerbeetle_client = TigerBeetleClient() diff --git a/billing_app/views.py b/billing_app/views.py new file mode 100644 index 0000000..4930e3f --- /dev/null +++ b/billing_app/views.py @@ -0,0 +1,30 @@ +from graphene_django.views import GraphQLView +from .graphql_middleware import M2MNoAuthMiddleware, PublicNoAuthMiddleware, BillingJWTMiddleware + + +class PublicGraphQLView(GraphQLView): + """GraphQL view for public operations (no authentication).""" + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [PublicNoAuthMiddleware()] + super().__init__(*args, **kwargs) + + +class M2MGraphQLView(GraphQLView): + """GraphQL view for M2M (machine-to-machine) operations. + No authentication required - used by internal services (Temporal, etc.) + """ + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [M2MNoAuthMiddleware()] + super().__init__(*args, **kwargs) + + +class TeamGraphQLView(GraphQLView): + """GraphQL view for team-level operations. + Requires Access token with teams:member scope. + """ + + def __init__(self, *args, **kwargs): + kwargs['middleware'] = [BillingJWTMiddleware()] + super().__init__(*args, **kwargs) diff --git a/create_accounts.py b/create_accounts.py new file mode 100644 index 0000000..241d136 --- /dev/null +++ b/create_accounts.py @@ -0,0 +1,11 @@ +from billing_app.models import Account +from django.db import transaction + +print("Creating sample accounts...") + +with transaction.atomic(): + for i in range(5): + account = Account.objects.create() + print(f"Created account: {account.uuid}") + +print("Sample accounts created successfully.") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bef7079 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +services: + billing: + build: + context: . + dockerfile: Dockerfile + container_name: billing + restart: unless-stopped + expose: + - "8000" + environment: + - PORT=8000 + - TB_CLUSTER_ID=0 + - TB_ADDRESS=tigerbeetle:3000 + security_opt: + - seccomp=unconfined + depends_on: + - tigerbeetle + networks: + dokploy-network: + aliases: + - billing + + tigerbeetle: + image: ghcr.io/tigerbeetle/tigerbeetle:latest + container_name: tigerbeetle + privileged: true + command: ["start", "--addresses=0.0.0.0:3000", "--development", "/var/lib/tigerbeetle/0_0.tigerbeetle"] + expose: + - "3000" + volumes: + - tigerbeetle_data:/var/lib/tigerbeetle + restart: unless-stopped + networks: + dokploy-network: + aliases: + - tigerbeetle + +volumes: + tigerbeetle_data: + +networks: + dokploy-network: + external: true diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..79d2824 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'billing.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/nixpacks.toml b/nixpacks.toml new file mode 100644 index 0000000..0e74127 --- /dev/null +++ b/nixpacks.toml @@ -0,0 +1,18 @@ +providers = ["python"] + +[build] + +[phases.install] +cmds = [ + "python -m venv --copies /opt/venv", + ". /opt/venv/bin/activate", + "pip install poetry==$NIXPACKS_POETRY_VERSION", + "poetry install --no-interaction --no-ansi" +] + +[start] +cmd = "poetry run python manage.py migrate && poetry run python manage.py collectstatic --noinput && poetry run python -m gunicorn billing.wsgi:application --bind 0.0.0.0:${PORT:-8000}" + +[variables] +# Set Poetry version to match local environment +NIXPACKS_POETRY_VERSION = "2.2.1" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..4f9783c --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1016 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "aenum" +version = "3.1.16" +description = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "aenum-3.1.16-py2-none-any.whl", hash = "sha256:7810cbb6b4054b7654e5a7bafbe16e9ee1d25ef8e397be699f63f2f3a5800433"}, + {file = "aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf"}, +] + +[[package]] +name = "asgiref" +version = "3.11.0" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, + {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, +] + +[package.extras] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] + +[[package]] +name = "boto3" +version = "1.42.6" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "boto3-1.42.6-py3-none-any.whl", hash = "sha256:69ff5cf6431fe7870da009f23aceabb20d56b4c9852ba9a808eaf6cc30ae02a5"}, + {file = "boto3-1.42.6.tar.gz", hash = "sha256:11dab889a24f378af6c93afd4aa06d7cace3866cbf02e78c7a77e9a7fb41967a"}, +] + +[package.dependencies] +botocore = ">=1.42.6,<1.43.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.16.0,<0.17.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.42.6" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "botocore-1.42.6-py3-none-any.whl", hash = "sha256:c4aebdc391f3542270ebea8b8f0060fde514f6441de207dce862ed759887607e"}, + {file = "botocore-1.42.6.tar.gz", hash = "sha256:ab389c6874dfbdc4c18de9b4a02d300cb6c7f6f2d4622c73e5965aeef80e570d"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.29.2)"] + +[[package]] +name = "certifi" +version = "2025.11.12" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "django" +version = "5.2.9" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "django-5.2.9-py3-none-any.whl", hash = "sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a"}, + {file = "django-5.2.9.tar.gz", hash = "sha256:16b5ccfc5e8c27e6c0561af551d2ea32852d7352c67d452ae3e76b4f6b2ca495"}, +] + +[package.dependencies] +asgiref = ">=3.8.1" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-cors-headers" +version = "4.9.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "django_cors_headers-4.9.0-py3-none-any.whl", hash = "sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449"}, + {file = "django_cors_headers-4.9.0.tar.gz", hash = "sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8"}, +] + +[package.dependencies] +asgiref = ">=3.6" +django = ">=4.2" + +[[package]] +name = "graphene" +version = "3.4.3" +description = "GraphQL Framework for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "graphene-3.4.3-py2.py3-none-any.whl", hash = "sha256:820db6289754c181007a150db1f7fff544b94142b556d12e3ebc777a7bf36c71"}, + {file = "graphene-3.4.3.tar.gz", hash = "sha256:2a3786948ce75fe7e078443d37f609cbe5bb36ad8d6b828740ad3b95ed1a0aaa"}, +] + +[package.dependencies] +graphql-core = ">=3.1,<3.3" +graphql-relay = ">=3.1,<3.3" +python-dateutil = ">=2.7.0,<3" +typing-extensions = ">=4.7.1,<5" + +[package.extras] +dev = ["coveralls (>=3.3,<5)", "mypy (>=1.10,<2)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)", "ruff (==0.5.0)", "types-python-dateutil (>=2.8.1,<3)"] +test = ["coveralls (>=3.3,<5)", "pytest (>=8,<9)", "pytest-asyncio (>=0.16,<2)", "pytest-benchmark (>=4,<5)", "pytest-cov (>=5,<6)", "pytest-mock (>=3,<4)"] + +[[package]] +name = "graphene-django" +version = "3.2.3" +description = "Graphene Django integration" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "graphene-django-3.2.3.tar.gz", hash = "sha256:d831bfe8e9a6e77e477b7854faef4addb318f386119a69ee4c57b74560f3e07d"}, + {file = "graphene_django-3.2.3-py2.py3-none-any.whl", hash = "sha256:0c673a4dad315b26b4d18eb379ad0c7027fd6a36d23a1848b7c7c09a14a9271e"}, +] + +[package.dependencies] +Django = ">=3.2" +graphene = ">=3.0,<4" +graphql-core = ">=3.1.0,<4" +graphql-relay = ">=3.1.1,<4" +promise = ">=2.1" +text-unidecode = "*" + +[package.extras] +dev = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", "mock", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-django (>=4.5.2)", "pytest-random-order", "pytz", "ruff (==0.1.2)"] +rest-framework = ["djangorestframework (>=3.6.3)"] +test = ["coveralls", "django-filter (>=22.1)", "djangorestframework (>=3.6.3)", "mock", "pytest (>=7.3.1)", "pytest-cov", "pytest-django (>=4.5.2)", "pytest-random-order", "pytz"] + +[[package]] +name = "graphql-core" +version = "3.2.7" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +optional = false +python-versions = "<4,>=3.7" +groups = ["main"] +files = [ + {file = "graphql_core-3.2.7-py3-none-any.whl", hash = "sha256:17fc8f3ca4a42913d8e24d9ac9f08deddf0a0b2483076575757f6c412ead2ec0"}, + {file = "graphql_core-3.2.7.tar.gz", hash = "sha256:27b6904bdd3b43f2a0556dad5d579bdfdeab1f38e8e8788e555bdcb586a6f62c"}, +] + +[[package]] +name = "graphql-relay" +version = "3.2.0" +description = "Relay library for graphql-core" +optional = false +python-versions = ">=3.6,<4" +groups = ["main"] +files = [ + {file = "graphql-relay-3.2.0.tar.gz", hash = "sha256:1ff1c51298356e481a0be009ccdff249832ce53f30559c1338f22a0e0d17250c"}, + {file = "graphql_relay-3.2.0-py3-none-any.whl", hash = "sha256:c9b22bd28b170ba1fe674c74384a8ff30a76c8e26f88ac3aa1584dd3179953e5"}, +] + +[package.dependencies] +graphql-core = ">=3.2,<3.3" + +[[package]] +name = "gunicorn" +version = "23.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "infisicalsdk" +version = "1.0.13" +description = "Official Infisical SDK for Python (Latest)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "infisicalsdk-1.0.13-py3-none-any.whl", hash = "sha256:bd67918e9c748d37fc8aed2925323f964462062d3877edc6d51883f29cc8fcd1"}, + {file = "infisicalsdk-1.0.13.tar.gz", hash = "sha256:baf85d51844f62f748e12e1ec451c06c27adf6256e97735272accc848174a950"}, +] + +[package.dependencies] +aenum = "*" +boto3 = ">=1.35,<2.0" +botocore = ">=1.35,<2.0" +python-dateutil = "*" +requests = ">=2.32,<3.0" + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "nexus-rpc" +version = "1.2.0" +description = "Nexus Python SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3"}, + {file = "nexus_rpc-1.2.0.tar.gz", hash = "sha256:b4ddaffa4d3996aaeadf49b80dfcdfbca48fe4cb616defaf3b3c5c2c8fc61890"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.2" + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "promise" +version = "2.3" +description = "Promises/A+ implementation for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "promise-2.3.tar.gz", hash = "sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["coveralls", "futures", "mock", "pytest (>=2.7.3)", "pytest-benchmark", "pytest-cov"] + +[[package]] +name = "protobuf" +version = "6.33.2" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d"}, + {file = "protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4"}, + {file = "protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43"}, + {file = "protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e"}, + {file = "protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872"}, + {file = "protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f"}, + {file = "protobuf-6.33.2-cp39-cp39-win32.whl", hash = "sha256:7109dcc38a680d033ffb8bf896727423528db9163be1b6a02d6a49606dcadbfe"}, + {file = "protobuf-6.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:2981c58f582f44b6b13173e12bb8656711189c2a70250845f264b877f00b1913"}, + {file = "protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c"}, + {file = "protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4"}, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.2.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "s3transfer" +version = "0.16.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe"}, + {file = "s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920"}, +] + +[package.dependencies] +botocore = ">=1.37.4,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] + +[[package]] +name = "sentry-sdk" +version = "2.47.0" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "sentry_sdk-2.47.0-py2.py3-none-any.whl", hash = "sha256:d72f8c61025b7d1d9e52510d03a6247b280094a327dd900d987717a4fce93412"}, + {file = "sentry_sdk-2.47.0.tar.gz", hash = "sha256:8218891d5e41b4ea8d61d2aed62ed10c80e39d9f2959d6f939efbf056857e050"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.11" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +anthropic = ["anthropic (>=0.16)"] +arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +celery-redbeat = ["celery-redbeat (>=2)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +google-genai = ["google-genai (>=1.29.0)"] +grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] +http2 = ["httpcore[http2] (==1.*)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] +langchain = ["langchain (>=0.0.210)"] +langgraph = ["langgraph (>=0.6.6)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] +litellm = ["litellm (>=1.77.5)"] +litestar = ["litestar (>=2.0.0)"] +loguru = ["loguru (>=0.5)"] +mcp = ["mcp (>=1.15.0)"] +openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro"] +opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure_eval"] +pydantic-ai = ["pydantic-ai (>=1.0.0)"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +statsig = ["statsig (>=0.55.3)"] +tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.4" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb"}, + {file = "sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e"}, +] + +[package.extras] +dev = ["build"] +doc = ["sphinx"] + +[[package]] +name = "temporalio" +version = "1.20.0" +description = "Temporal.io Python SDK" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "temporalio-1.20.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:fba70314b4068f8b1994bddfa0e2ad742483f0ae714d2ef52e63013ccfd7042e"}, + {file = "temporalio-1.20.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffc5bb6cabc6ae67f0bfba44de6a9c121603134ae18784a2ff3a7f230ad99080"}, + {file = "temporalio-1.20.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e80c1e4cdf88fa8277177f563edc91466fe4dc13c0322f26e55c76b6a219e6"}, + {file = "temporalio-1.20.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba92d909188930860c9d89ca6d7a753bc5a67e4e9eac6cea351477c967355eed"}, + {file = "temporalio-1.20.0-cp310-abi3-win_amd64.whl", hash = "sha256:eacfd571b653e0a0f4aa6593f4d06fc628797898f0900d400e833a1f40cad03a"}, + {file = "temporalio-1.20.0.tar.gz", hash = "sha256:5a6a85b7d298b7359bffa30025f7deac83c74ac095a4c6952fbf06c249a2a67c"}, +] + +[package.dependencies] +nexus-rpc = "1.2.0" +protobuf = ">=3.20,<7.0.0" +types-protobuf = ">=3.20" +typing-extensions = ">=4.2.0,<5" + +[package.extras] +grpc = ["grpcio (>=1.48.2,<2)"] +openai-agents = ["mcp (>=1.9.4,<2)", "openai-agents (>=0.3,<0.5)"] +opentelemetry = ["opentelemetry-api (>=1.11.1,<2)", "opentelemetry-sdk (>=1.11.1,<2)"] +pydantic = ["pydantic (>=2.0.0,<3)"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + +[[package]] +name = "tigerbeetle" +version = "0.16.66" +description = "The TigerBeetle client for Python." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tigerbeetle-0.16.66-py3-none-any.whl", hash = "sha256:e622a35efc627601360fd3b95d8e5d4bf45eb00ce41a484fdfe0ff783754be7f"}, +] + +[[package]] +name = "types-protobuf" +version = "6.32.1.20251210" +description = "Typing stubs for protobuf" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140"}, + {file = "types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "urllib3" +version = "2.6.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b"}, + {file = "urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "whitenoise" +version = "6.11.0" +description = "Radically simplified static file serving for WSGI applications" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258"}, + {file = "whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f"}, +] + +[package.extras] +brotli = ["brotli"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "6f9cb6fca9510889fb1af9c42487033e076e5851dd219939e2eca2c3a7500d1e" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..22953fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "billing" +version = "0.1.0" +description = "Billing service for Optovia" +authors = [ + {name = "Ruslan Bakiev",email = "572431+veikab@users.noreply.github.com"} +] +readme = "README.md" +requires-python = "^3.11" +dependencies = [ + "django (>=5.2.8,<6.0)", + "graphene-django (>=3.2.3,<4.0.0)", + "django-cors-headers (>=4.9.0,<5.0.0)", + "psycopg2-binary (>=2.9.11,<3.0.0)", + "requests (>=2.32.5,<3.0.0)", + "temporalio (>=1.4.0,<2.0.0)", + "python-dotenv (>=1.2.1,<2.0.0)", + "pyjwt (>=2.10.1,<3.0.0)", + "cryptography (>=46.0.3,<47.0.0)", + "infisicalsdk (>=1.0.12,<2.0.0)", + "gunicorn (>=23.0.0,<24.0.0)", + "whitenoise (>=6.7.0,<7.0.0)", + "sentry-sdk (>=2.47.0,<3.0.0)", + "tigerbeetle (>=0.1.1,<1.0.0)" +] + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/test_tb_connection.py b/test_tb_connection.py new file mode 100644 index 0000000..5c52bca --- /dev/null +++ b/test_tb_connection.py @@ -0,0 +1,53 @@ +import os +import uuid +from tigerbeetle import Account, ClientSync +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Ensure these are set correctly in your environment or .env file +CLUSTER_ID = int(os.getenv("TB_CLUSTER_ID", "0")) +TB_ADDRESS = os.getenv("TB_ADDRESS", "127.0.0.1:3000") + +def test_tigerbeetle_connection(): + client = None + try: + logger.info(f"Attempting to connect to TigerBeetle cluster {CLUSTER_ID} at {TB_ADDRESS}...") + client = ClientSync(CLUSTER_ID, TB_ADDRESS) + logger.info("Successfully connected to TigerBeetle.") + + # Try to create a dummy account to verify functionality + test_account_id = uuid.uuid4().int # Use uuid.int for 128-bit ID + + account_to_create = Account( + id=test_account_id, + ledger=1, # Example ledger + code=100, # Example code + ) + + logger.info(f"Attempting to create a test account with ID: {test_account_id}...") + errors = client.create_accounts([account_to_create]) + + if errors: + for error in errors: + logger.error(f"Error creating test account {error.index}: {error.code.name}") + return False, f"Failed to create test account: {', '.join([e.code.name for e in errors])}" + else: + logger.info("Test account created successfully.") + return True, "Connection and basic account creation successful." + + except Exception as e: + logger.error(f"Failed to connect or interact with TigerBeetle: {e}") + return False, str(e) + finally: + if client: + client.close() + logger.info("TigerBeetle client closed.") + +if __name__ == "__main__": + success, message = test_tigerbeetle_connection() + if success: + logger.info(f"TigerBeetle connection check PASSED: {message}") + else: + logger.error(f"TigerBeetle connection check FAILED: {message}") diff --git a/transactions_script.py b/transactions_script.py new file mode 100644 index 0000000..72b4242 --- /dev/null +++ b/transactions_script.py @@ -0,0 +1,183 @@ +import os +import uuid +import tigerbeetle as tb +from django.contrib.auth import get_user_model +from django.conf import settings +from billing_app.models import Account as AccountModel +from billing_app.tigerbeetle_client import tigerbeetle_client + +# Ensure Django settings are configured if not already +if not settings.configured: + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'billing.settings') + import django + django.setup() + +# Define Ledger and Code (these are example values, adjust as needed) +LEDGER = 700 +CODE = 10 + +print("--- Starting Raw Data Display for Multiple Transactions ---") + +# 1. Create source, destination, and bank accounts +print("Creating source, destination, and bank accounts...") +source_account_id = uuid.uuid4() +destination_account_id = uuid.uuid4() +bank_account_id = uuid.uuid4() # Bank account for initial funding + +# Create in local DB +source_local_account = AccountModel.objects.create(uuid=source_account_id) +destination_local_account = AccountModel.objects.create(uuid=destination_account_id) +bank_local_account = AccountModel.objects.create(uuid=bank_account_id) + +# Prepare for TigerBeetle +accounts_to_create_details = [ + { + "id": source_account_id.int, + "ledger": LEDGER, + "code": CODE, + "flags": tb.AccountFlags.NONE # All flags set to NONE for simplicity + }, + { + "id": destination_account_id.int, + "ledger": LEDGER, + "code": CODE, + "flags": tb.AccountFlags.NONE # All flags set to NONE for simplicity + }, + { + "id": bank_account_id.int, + "ledger": LEDGER, + "code": CODE, + "flags": tb.AccountFlags.NONE # Changed: Removed CREDIT_RESERVED, set to NONE + }, +] + +# Create accounts in TigerBeetle using create_account (singular) for each +account_creation_results = [] +for acc_details in accounts_to_create_details: + flags = acc_details.get("flags", tb.AccountFlags.NONE) + results = tigerbeetle_client.create_account( + acc_details["id"], + acc_details["ledger"], + acc_details["code"], + flags + ) + if results: # If non-empty, it means there are errors + account_creation_results.extend(results) + +if account_creation_results: + print(f"Failed to create accounts in TigerBeetle. Raw results: {account_creation_results}") # Print raw results + import sys + sys.exit(1) +else: + print(f"Source account created: {source_account_id}") + print(f"Destination account created: {destination_account_id}") + print(f"Bank account created: {bank_account_id}") + +# Store all transfer IDs for later lookup +all_transfer_ids = [] + +# 2. Fund the source account (initial balance) +print("Funding source account with initial balance...") +initial_fund_id = uuid.uuid4() +initial_fund_transfer = tb.Transfer( + id=initial_fund_id.int, + debit_account_id=bank_account_id.int, # Use real bank account ID + credit_account_id=source_account_id.int, + amount=1000, # Initial amount + ledger=LEDGER, + code=CODE, +) +fund_results = tigerbeetle_client.create_transfer( + initial_fund_transfer.id, + initial_fund_transfer.debit_account_id, + initial_fund_transfer.credit_account_id, + initial_fund_transfer.amount, + initial_fund_transfer.ledger, + initial_fund_transfer.code, + initial_fund_transfer.flags +) +if fund_results: # If non-empty, it means there are errors + print(f"Failed to fund source account. Raw results: {fund_results}") + import sys + sys.exit(1) +else: + print(f"Source account funded with 1000. Transfer ID: {initial_fund_id}") + all_transfer_ids.append(initial_fund_id.int) # Store ID + +# 3. Perform 5 transactions from source to destination +print("Performing 5 transactions from source to destination...") +for i in range(1, 6): + transfer_id = uuid.uuid4() + transfer_amount = 100 # Example amount for each transfer + + transfer = tb.Transfer( # tb.Transfer object + id=transfer_id.int, + debit_account_id=source_account_id.int, + credit_account_id=destination_account_id.int, + amount=transfer_amount, + ledger=LEDGER, + code=CODE, + ) + transfer_results = tigerbeetle_client.create_transfer( + transfer.id, + transfer.debit_account_id, + transfer.credit_account_id, + transfer.amount, + transfer.ledger, + transfer.code, + transfer.flags + ) + if transfer_results: + print(f"Transaction {i} failed. Raw results: {transfer_results}") + else: + print(f"Transaction {i} (Amount: {transfer_amount}) completed. Transfer ID: {transfer.id}") + all_transfer_ids.append(transfer_id.int) # Store ID + +# 4. Query detailed account information from TigerBeetle +print("\n--- Raw Account Details (from TigerBeetle) ---") +account_ids_to_lookup = [source_account_id.int, destination_account_id.int, bank_account_id.int] +detailed_accounts = tigerbeetle_client.lookup_accounts(account_ids_to_lookup) + +for account in detailed_accounts: + if isinstance(account, tb.Account): + print(f"Account ID: {uuid.UUID(int=account.id)}") + print(f" User Data 128: {account.user_data_128}") + print(f" User Data 64: {account.user_data_64}") + print(f" User Data 32: {account.user_data_32}") + print(f" Ledger: {account.ledger}") + print(f" Code: {account.code}") + print(f" Flags: {account.flags}") + print(f" Timestamp: {account.timestamp}") + print(f" Debits Posted: {account.debits_posted}") + print(f" Credits Posted: {account.credits_posted}") + print(f" Debits Pending: {account.debits_pending}") + print(f" Credits Pending: {account.credits_pending}") + print(f" Net Balance: {account.credits_posted - account.debits_posted}") + print("-" * 30) + else: + print(f"Could not retrieve details for account ID: {account.id if hasattr(account, 'id') else 'Unknown'}") + +# 5. Query ALL transfer information +print("\n--- Raw Transfer Details (from TigerBeetle) ---") +detailed_transfers = tigerbeetle_client._client.lookup_transfers(all_transfer_ids) + +for transfer_detail in detailed_transfers: + if isinstance(transfer_detail, tb.Transfer): + print(f"Transfer ID: {uuid.UUID(int=transfer_detail.id)}") + print(f" Debit Account ID: {uuid.UUID(int=transfer_detail.debit_account_id)}") + print(f" Credit Account ID: {uuid.UUID(int=transfer_detail.credit_account_id)}") + print(f" Amount: {transfer_detail.amount}") + print(f" Pending ID: {transfer_detail.pending_id}") + print(f" User Data 128: {transfer_detail.user_data_128}") + print(f" User Data 64: {transfer_detail.user_data_64}") + print(f" User Data 32: {transfer_detail.user_data_32}") + print(f" Timeout: {transfer_detail.timeout}") + print(f" Ledger: {transfer_detail.ledger}") + print(f" Code: {transfer_detail.code}") + print(f" Flags: {transfer_detail.flags}") + print(f" Timestamp: {transfer_detail.timestamp}") + print("-" * 30) + else: + print(f"Could not retrieve details for transfer ID: {transfer_detail.id if hasattr(transfer_detail, 'id') else 'Unknown'}") + +print("\n--- Raw Data Display Complete ---")