From e51d8d6ff9f6eb47428bc9b81ce47dffff09f3fd Mon Sep 17 00:00:00 2001 From: Trey <73353716+TreyWW@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:06:04 +0000 Subject: [PATCH 01/15] Initial Commit Moved to "core" namespace Fixed team dropdown style issues Fixed migration signals, converted core:dashboard to just dashboard Added github actions + templates --- .github/FUNDING.yml | 13 + .github/ISSUE_TEMPLATE/1-bug_report.yml | 37 + .github/ISSUE_TEMPLATE/2-ideas.yml | 49 + .github/ISSUE_TEMPLATE/config.yml | 14 + .github/dependabot.yml | 21 + .github/pull_request_template.md | 7 + .github/workflows/check_typos.yml | 36 + .github/workflows/codeql.yml | 81 + .github/workflows/dependency-review.yml | 39 + .github/workflows/docs.yml.disabled | 34 + .github/workflows/publish_to_pypi.yml | 49 + .github/workflows/run_black_linter.yml | 18 + .github/workflows/run_djlint.yml | 55 + .github/workflows/run_mypy.yml | 47 + .github/workflows/stale_prs.yml | 21 + .github/workflows/tests.yml | 83 + .gitignore | 153 + README.md | 2 + makemigrations.py | 8 + package-lock.json | 1308 ++++++ package.json | 10 + poetry.lock | 3494 +++++++++++++++++ pyproject.toml | 183 + src/billing/__init__.py | 1 + src/billing/admin.py | 3 + src/billing/apps.py | 21 + src/billing/billing_settings.py | 43 + src/billing/data/__init__.py | 0 src/billing/data/default_usage_plans.py | 135 + src/billing/decorators.py | 32 + src/billing/management/__init__.py | 0 src/billing/management/commands/__init__.py | 0 src/billing/management/commands/stripe.py | 43 + src/billing/middleware.py | 50 + src/billing/migrations/0001_initial.py | 192 + src/billing/migrations/0002_initial.py | 155 + src/billing/migrations/__init__.py | 0 src/billing/models.py | 117 + src/billing/service/__init__.py | 0 src/billing/service/checkout_completed.py | 48 + src/billing/service/entitlements.py | 55 + src/billing/service/get_user.py | 8 + src/billing/service/plan_change.py | 31 + src/billing/service/price.py | 8 + src/billing/service/stripe_customer.py | 32 + src/billing/service/subscription_ended.py | 39 + src/billing/service/subscription_handler.py | 86 + src/billing/service/test.py | 38 + src/billing/signals/__init__.py | 1 + src/billing/signals/migrations.py | 69 + src/billing/signals/quotas.py | 15 + src/billing/signals/stripe/__init__.py | 1 + src/billing/signals/stripe/webhook_handler.py | 19 + src/billing/signals/usage.py | 41 + .../billing/dashboard/all_subscriptions.html | 71 + .../dashboard/choose_plan_section.html | 199 + .../pages/billing/dashboard/dashboard.html | 15 + .../billing/dashboard/growth_usages.html | 137 + .../billing/dashboard/starter_usages.html | 73 + src/billing/urls.py | 21 + src/billing/views.py | 3 + src/billing/views/__init__.py | 0 src/billing/views/change_plan.py | 111 + src/billing/views/dashboard.py | 50 + src/billing/views/return_urls/failed.py | 9 + src/billing/views/return_urls/success.py | 7 + src/billing/views/stripe_misc.py | 30 + src/billing/views/stripe_webhooks.py | 33 + src/core/__init__.py | 0 src/core/admin.py | 76 + src/core/api/__init__.py | 0 src/core/api/base/__init__.py | 0 src/core/api/base/breadcrumbs.py | 19 + src/core/api/base/notifications.py | 41 + src/core/api/base/urls.py | 27 + src/core/api/emails/__init__.py | 0 src/core/api/emails/fetch.py | 43 + src/core/api/emails/status.py | 99 + src/core/api/emails/urls.py | 24 + src/core/api/healthcheck/__init__.py | 0 src/core/api/healthcheck/healthcheck.py | 17 + src/core/api/healthcheck/urls.py | 18 + src/core/api/landing_page/__init__.py | 0 src/core/api/landing_page/email_waitlist.py | 49 + src/core/api/landing_page/urls.py | 8 + src/core/api/maintenance/__init__.py | 0 src/core/api/maintenance/now.py | 31 + src/core/api/maintenance/urls.py | 9 + src/core/api/public/__init__.py | 1 + src/core/api/public/authentication.py | 66 + src/core/api/public/decorators.py | 44 + src/core/api/public/endpoints/__init__.py | 0 .../api/public/endpoints/system_health.py | 63 + .../api/public/endpoints/webhooks/__init__.py | 0 .../api/public/endpoints/webhooks/urls.py | 12 + .../webhooks/webhook_task_queue_handler.py | 46 + src/core/api/public/helpers/__init__.py | 0 src/core/api/public/helpers/deprecate.py | 46 + src/core/api/public/helpers/response.py | 19 + src/core/api/public/middleware.py | 0 src/core/api/public/models.py | 69 + src/core/api/public/permissions.py | 66 + src/core/api/public/serializers/__init__.py | 0 src/core/api/public/swagger_ui.py | 42 + src/core/api/public/types.py | 12 + src/core/api/public/urls.py | 18 + src/core/api/quotas/__init__.py | 0 src/core/api/quotas/fetch.py | 30 + src/core/api/quotas/requests.py | 136 + src/core/api/quotas/urls.py | 16 + src/core/api/settings/__init__.py | 0 src/core/api/settings/api_keys.py | 69 + src/core/api/settings/change_name.py | 40 + src/core/api/settings/preferences.py | 48 + src/core/api/settings/profile_picture.py | 29 + src/core/api/settings/urls.py | 22 + src/core/api/teams/__init__.py | 0 src/core/api/teams/create.py | 36 + src/core/api/teams/create_user.py | 37 + src/core/api/teams/edit_permissions.py | 31 + src/core/api/teams/invites.py | 193 + src/core/api/teams/kick.py | 33 + src/core/api/teams/leave.py | 32 + src/core/api/teams/switch_team.py | 53 + src/core/api/teams/urls.py | 58 + src/core/api/urls.py | 19 + src/core/apps.py | 15 + src/core/backend_utils/__init__.py | 0 src/core/boto3/__init__.py | 0 src/core/boto3/handler.py | 86 + src/core/config.py | 31 + src/core/data/__init__.py | 0 src/core/data/default_email_templates.py | 71 + src/core/data/default_feature_flags.py | 26 + src/core/data/default_quota_limits.py | 186 + src/core/decorators.py | 272 ++ src/core/manage.py | 21 + src/core/management/__init__.py | 0 src/core/management/commands/__init__.py | 0 src/core/management/commands/auto.py | 12 + .../management/commands/contributors.json | 266 ++ src/core/management/commands/contributors.py | 295 ++ src/core/management/commands/feature_flags.py | 55 + .../commands/generate_aws_scheduler_apikey.py | 32 + src/core/management/commands/lint.py | 32 + src/core/management/commands/list_urls.py | 11 + .../management/commands/navbar_refresh.py | 12 + src/core/management/commands/test_urls.py | 9 + src/core/management/commands/test_views.py | 33 + src/core/migrations/0001_initial.py | 783 ++++ src/core/migrations/__init__.py | 0 src/core/models.py | 504 +++ src/core/service/__init__.py | 0 src/core/service/api_keys/__init__.py | 0 src/core/service/api_keys/delete.py | 18 + src/core/service/api_keys/generate.py | 103 + src/core/service/api_keys/get.py | 10 + src/core/service/asyn_tasks/__init__.py | 0 src/core/service/asyn_tasks/tasks.py | 95 + src/core/service/base/__init__.py | 0 src/core/service/base/breadcrumbs.py | 85 + src/core/service/boto3/__init__.py | 0 src/core/service/boto3/handler.py | 84 + src/core/service/emails/__init__.py | 0 src/core/service/maintenance/__init__.py | 0 .../service/maintenance/expire/__init__.py | 0 src/core/service/maintenance/expire/run.py | 48 + src/core/service/maintenance/tasks.py | 18 + src/core/service/modals/__init__.py | 0 src/core/service/modals/modals.py | 106 + src/core/service/modals/registry.py | 77 + src/core/service/modals/template_exists.py | 0 src/core/service/permissions/__init__.py | 0 src/core/service/permissions/scopes.py | 34 + src/core/service/settings/__init__.py | 0 src/core/service/settings/update.py | 36 + src/core/service/settings/view.py | 55 + src/core/service/teams/__init__.py | 0 src/core/service/teams/create_user.py | 65 + src/core/service/teams/fetch.py | 8 + src/core/service/teams/permissions.py | 47 + src/core/service/webhooks/__init__.py | 0 src/core/service/webhooks/auth.py | 33 + src/core/settings.py | 28 + src/core/signals/__init__.py | 4 + src/core/signals/migrations.py | 70 + src/core/signals/signals.py | 88 + .../templates/core/base/+left_drawer.html | 84 + src/core/templates/core/base/_head.html | 70 + src/core/templates/core/base/auth.html | 41 + src/core/templates/core/base/base.html | 55 + src/core/templates/core/base/breadcrumbs.html | 14 + .../templates/core/base/breadcrumbs_ul.html | 11 + src/core/templates/core/base/htmx.html | 11 + src/core/templates/core/base/toast.html | 1 + src/core/templates/core/base/toasts.html | 1 + .../core/base/topbar/+icon_dropdown.html | 47 + .../core/base/topbar/_notification_count.html | 6 + .../topbar/_notification_dropdown_items.html | 89 + .../core/base/topbar/_organizations_list.html | 15 + .../templates/core/base/topbar/_topbar.html | 128 + .../base/topbar/team_selector/selector.html | 52 + src/core/templates/modals/accept_invite.html | 22 + .../modals/change_profile_picture.html | 45 + .../modals/create_invoice_product.html | 63 + .../templates/modals/create_reminder.html | 67 + src/core/templates/modals/create_team.html | 27 + .../modals/edit_team_member_permissions.html | 36 + .../templates/modals/generate_api_key.html | 98 + .../templates/modals/generate_report.html | 79 + src/core/templates/modals/invite_user.html | 33 + .../modals/invoices_add_service.html | 64 + .../modals/invoices_edit_discount.html | 79 + .../modals/invoices_from_destination.html | 69 + .../modals/invoices_to_destination.html | 125 + src/core/templates/modals/leave_team.html | 22 + .../templates/modals/receipts_upload.html | 81 + .../templates/modals/send_bulk_email.html | 96 + .../templates/modals/send_single_email.html | 70 + .../templates/modals/team_create_user.html | 67 + .../modals/view_quota_limit_info.html | 78 + src/core/templatetags/__init__.py | 0 src/core/templatetags/cal_filters.py | 16 + src/core/templatetags/dictfilters.py | 13 + src/core/templatetags/feature_enabled.py | 46 + src/core/templatetags/listfilters.py | 53 + src/core/templatetags/strfilters.py | 59 + src/core/templatetags/utils.py | 42 + src/core/types/__init__.py | 0 src/core/types/emails.py | 45 + src/core/types/htmx.py | 24 + src/core/types/requests.py | 19 + src/core/urls.py | 45 + src/core/utils/__init__.py | 0 src/core/utils/calendar.py | 24 + src/core/utils/dataclasses.py | 134 + src/core/utils/feature_flags.py | 29 + src/core/utils/http_utils.py | 16 + src/core/utils/quota_limit_ops.py | 42 + src/core/utils/service_retry.py | 19 + src/core/utils/settings_helper.py | 284 ++ src/core/views/__init__.py | 0 src/core/views/auth/__init__.py | 0 src/core/views/auth/create_account.py | 94 + src/core/views/auth/helpers.py | 3 + src/core/views/auth/login.py | 274 ++ src/core/views/auth/passwords/__init__.py | 0 src/core/views/auth/passwords/generate.py | 105 + src/core/views/auth/passwords/set.py | 45 + src/core/views/auth/passwords/view.py | 21 + src/core/views/auth/urls.py | 75 + src/core/views/auth/verify.py | 92 + src/core/views/emails/__init__.py | 0 src/core/views/emails/dashboard.py | 12 + src/core/views/emails/urls.py | 15 + src/core/views/modals/__init__.py | 0 src/core/views/modals/open.py | 14 + src/core/views/other/__init__.py | 2 + src/core/views/other/errors.py | 63 + src/core/views/other/index.py | 16 + src/core/views/quotas/__init__.py | 0 src/core/views/quotas/view.py | 28 + src/core/views/settings/__init__.py | 0 src/core/views/settings/teams.py | 81 + src/core/views/settings/urls.py | 15 + src/core/views/settings/view.py | 84 + src/core/views/teams/__init__.py | 0 src/core/views/teams/urls.py | 13 + src/makemigrations.py | 8 + src/settings.py | 28 + static/src/input.css | 259 ++ static/tailwind.config.js | 103 + 272 files changed, 17721 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/1-bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/2-ideas.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/check_typos.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/docs.yml.disabled create mode 100644 .github/workflows/publish_to_pypi.yml create mode 100644 .github/workflows/run_black_linter.yml create mode 100644 .github/workflows/run_djlint.yml create mode 100644 .github/workflows/run_mypy.yml create mode 100644 .github/workflows/stale_prs.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 makemigrations.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/billing/__init__.py create mode 100644 src/billing/admin.py create mode 100644 src/billing/apps.py create mode 100644 src/billing/billing_settings.py create mode 100644 src/billing/data/__init__.py create mode 100644 src/billing/data/default_usage_plans.py create mode 100644 src/billing/decorators.py create mode 100644 src/billing/management/__init__.py create mode 100644 src/billing/management/commands/__init__.py create mode 100644 src/billing/management/commands/stripe.py create mode 100644 src/billing/middleware.py create mode 100644 src/billing/migrations/0001_initial.py create mode 100644 src/billing/migrations/0002_initial.py create mode 100644 src/billing/migrations/__init__.py create mode 100644 src/billing/models.py create mode 100644 src/billing/service/__init__.py create mode 100644 src/billing/service/checkout_completed.py create mode 100644 src/billing/service/entitlements.py create mode 100644 src/billing/service/get_user.py create mode 100644 src/billing/service/plan_change.py create mode 100644 src/billing/service/price.py create mode 100644 src/billing/service/stripe_customer.py create mode 100644 src/billing/service/subscription_ended.py create mode 100644 src/billing/service/subscription_handler.py create mode 100644 src/billing/service/test.py create mode 100644 src/billing/signals/__init__.py create mode 100644 src/billing/signals/migrations.py create mode 100644 src/billing/signals/quotas.py create mode 100644 src/billing/signals/stripe/__init__.py create mode 100644 src/billing/signals/stripe/webhook_handler.py create mode 100644 src/billing/signals/usage.py create mode 100644 src/billing/templates/pages/billing/dashboard/all_subscriptions.html create mode 100644 src/billing/templates/pages/billing/dashboard/choose_plan_section.html create mode 100644 src/billing/templates/pages/billing/dashboard/dashboard.html create mode 100644 src/billing/templates/pages/billing/dashboard/growth_usages.html create mode 100644 src/billing/templates/pages/billing/dashboard/starter_usages.html create mode 100644 src/billing/urls.py create mode 100644 src/billing/views.py create mode 100644 src/billing/views/__init__.py create mode 100644 src/billing/views/change_plan.py create mode 100644 src/billing/views/dashboard.py create mode 100644 src/billing/views/return_urls/failed.py create mode 100644 src/billing/views/return_urls/success.py create mode 100644 src/billing/views/stripe_misc.py create mode 100644 src/billing/views/stripe_webhooks.py create mode 100644 src/core/__init__.py create mode 100644 src/core/admin.py create mode 100644 src/core/api/__init__.py create mode 100644 src/core/api/base/__init__.py create mode 100644 src/core/api/base/breadcrumbs.py create mode 100644 src/core/api/base/notifications.py create mode 100644 src/core/api/base/urls.py create mode 100644 src/core/api/emails/__init__.py create mode 100644 src/core/api/emails/fetch.py create mode 100644 src/core/api/emails/status.py create mode 100644 src/core/api/emails/urls.py create mode 100644 src/core/api/healthcheck/__init__.py create mode 100644 src/core/api/healthcheck/healthcheck.py create mode 100644 src/core/api/healthcheck/urls.py create mode 100644 src/core/api/landing_page/__init__.py create mode 100644 src/core/api/landing_page/email_waitlist.py create mode 100644 src/core/api/landing_page/urls.py create mode 100644 src/core/api/maintenance/__init__.py create mode 100644 src/core/api/maintenance/now.py create mode 100644 src/core/api/maintenance/urls.py create mode 100644 src/core/api/public/__init__.py create mode 100644 src/core/api/public/authentication.py create mode 100644 src/core/api/public/decorators.py create mode 100644 src/core/api/public/endpoints/__init__.py create mode 100644 src/core/api/public/endpoints/system_health.py create mode 100644 src/core/api/public/endpoints/webhooks/__init__.py create mode 100644 src/core/api/public/endpoints/webhooks/urls.py create mode 100644 src/core/api/public/endpoints/webhooks/webhook_task_queue_handler.py create mode 100644 src/core/api/public/helpers/__init__.py create mode 100644 src/core/api/public/helpers/deprecate.py create mode 100644 src/core/api/public/helpers/response.py create mode 100644 src/core/api/public/middleware.py create mode 100644 src/core/api/public/models.py create mode 100644 src/core/api/public/permissions.py create mode 100644 src/core/api/public/serializers/__init__.py create mode 100644 src/core/api/public/swagger_ui.py create mode 100644 src/core/api/public/types.py create mode 100644 src/core/api/public/urls.py create mode 100644 src/core/api/quotas/__init__.py create mode 100644 src/core/api/quotas/fetch.py create mode 100644 src/core/api/quotas/requests.py create mode 100644 src/core/api/quotas/urls.py create mode 100644 src/core/api/settings/__init__.py create mode 100644 src/core/api/settings/api_keys.py create mode 100644 src/core/api/settings/change_name.py create mode 100644 src/core/api/settings/preferences.py create mode 100644 src/core/api/settings/profile_picture.py create mode 100644 src/core/api/settings/urls.py create mode 100644 src/core/api/teams/__init__.py create mode 100644 src/core/api/teams/create.py create mode 100644 src/core/api/teams/create_user.py create mode 100644 src/core/api/teams/edit_permissions.py create mode 100644 src/core/api/teams/invites.py create mode 100644 src/core/api/teams/kick.py create mode 100644 src/core/api/teams/leave.py create mode 100644 src/core/api/teams/switch_team.py create mode 100644 src/core/api/teams/urls.py create mode 100644 src/core/api/urls.py create mode 100644 src/core/apps.py create mode 100644 src/core/backend_utils/__init__.py create mode 100644 src/core/boto3/__init__.py create mode 100644 src/core/boto3/handler.py create mode 100644 src/core/config.py create mode 100644 src/core/data/__init__.py create mode 100644 src/core/data/default_email_templates.py create mode 100644 src/core/data/default_feature_flags.py create mode 100644 src/core/data/default_quota_limits.py create mode 100644 src/core/decorators.py create mode 100644 src/core/manage.py create mode 100644 src/core/management/__init__.py create mode 100644 src/core/management/commands/__init__.py create mode 100644 src/core/management/commands/auto.py create mode 100644 src/core/management/commands/contributors.json create mode 100644 src/core/management/commands/contributors.py create mode 100644 src/core/management/commands/feature_flags.py create mode 100644 src/core/management/commands/generate_aws_scheduler_apikey.py create mode 100644 src/core/management/commands/lint.py create mode 100644 src/core/management/commands/list_urls.py create mode 100644 src/core/management/commands/navbar_refresh.py create mode 100644 src/core/management/commands/test_urls.py create mode 100644 src/core/management/commands/test_views.py create mode 100644 src/core/migrations/0001_initial.py create mode 100644 src/core/migrations/__init__.py create mode 100644 src/core/models.py create mode 100644 src/core/service/__init__.py create mode 100644 src/core/service/api_keys/__init__.py create mode 100644 src/core/service/api_keys/delete.py create mode 100644 src/core/service/api_keys/generate.py create mode 100644 src/core/service/api_keys/get.py create mode 100644 src/core/service/asyn_tasks/__init__.py create mode 100644 src/core/service/asyn_tasks/tasks.py create mode 100644 src/core/service/base/__init__.py create mode 100644 src/core/service/base/breadcrumbs.py create mode 100644 src/core/service/boto3/__init__.py create mode 100644 src/core/service/boto3/handler.py create mode 100644 src/core/service/emails/__init__.py create mode 100644 src/core/service/maintenance/__init__.py create mode 100644 src/core/service/maintenance/expire/__init__.py create mode 100644 src/core/service/maintenance/expire/run.py create mode 100644 src/core/service/maintenance/tasks.py create mode 100644 src/core/service/modals/__init__.py create mode 100644 src/core/service/modals/modals.py create mode 100644 src/core/service/modals/registry.py create mode 100644 src/core/service/modals/template_exists.py create mode 100644 src/core/service/permissions/__init__.py create mode 100644 src/core/service/permissions/scopes.py create mode 100644 src/core/service/settings/__init__.py create mode 100644 src/core/service/settings/update.py create mode 100644 src/core/service/settings/view.py create mode 100644 src/core/service/teams/__init__.py create mode 100644 src/core/service/teams/create_user.py create mode 100644 src/core/service/teams/fetch.py create mode 100644 src/core/service/teams/permissions.py create mode 100644 src/core/service/webhooks/__init__.py create mode 100644 src/core/service/webhooks/auth.py create mode 100644 src/core/settings.py create mode 100644 src/core/signals/__init__.py create mode 100644 src/core/signals/migrations.py create mode 100644 src/core/signals/signals.py create mode 100644 src/core/templates/core/base/+left_drawer.html create mode 100644 src/core/templates/core/base/_head.html create mode 100644 src/core/templates/core/base/auth.html create mode 100644 src/core/templates/core/base/base.html create mode 100644 src/core/templates/core/base/breadcrumbs.html create mode 100644 src/core/templates/core/base/breadcrumbs_ul.html create mode 100644 src/core/templates/core/base/htmx.html create mode 100644 src/core/templates/core/base/toast.html create mode 100644 src/core/templates/core/base/toasts.html create mode 100644 src/core/templates/core/base/topbar/+icon_dropdown.html create mode 100644 src/core/templates/core/base/topbar/_notification_count.html create mode 100644 src/core/templates/core/base/topbar/_notification_dropdown_items.html create mode 100644 src/core/templates/core/base/topbar/_organizations_list.html create mode 100644 src/core/templates/core/base/topbar/_topbar.html create mode 100644 src/core/templates/core/base/topbar/team_selector/selector.html create mode 100644 src/core/templates/modals/accept_invite.html create mode 100644 src/core/templates/modals/change_profile_picture.html create mode 100644 src/core/templates/modals/create_invoice_product.html create mode 100644 src/core/templates/modals/create_reminder.html create mode 100644 src/core/templates/modals/create_team.html create mode 100644 src/core/templates/modals/edit_team_member_permissions.html create mode 100644 src/core/templates/modals/generate_api_key.html create mode 100644 src/core/templates/modals/generate_report.html create mode 100644 src/core/templates/modals/invite_user.html create mode 100644 src/core/templates/modals/invoices_add_service.html create mode 100644 src/core/templates/modals/invoices_edit_discount.html create mode 100644 src/core/templates/modals/invoices_from_destination.html create mode 100644 src/core/templates/modals/invoices_to_destination.html create mode 100644 src/core/templates/modals/leave_team.html create mode 100644 src/core/templates/modals/receipts_upload.html create mode 100644 src/core/templates/modals/send_bulk_email.html create mode 100644 src/core/templates/modals/send_single_email.html create mode 100644 src/core/templates/modals/team_create_user.html create mode 100644 src/core/templates/modals/view_quota_limit_info.html create mode 100644 src/core/templatetags/__init__.py create mode 100644 src/core/templatetags/cal_filters.py create mode 100644 src/core/templatetags/dictfilters.py create mode 100644 src/core/templatetags/feature_enabled.py create mode 100644 src/core/templatetags/listfilters.py create mode 100644 src/core/templatetags/strfilters.py create mode 100644 src/core/templatetags/utils.py create mode 100644 src/core/types/__init__.py create mode 100644 src/core/types/emails.py create mode 100644 src/core/types/htmx.py create mode 100644 src/core/types/requests.py create mode 100644 src/core/urls.py create mode 100644 src/core/utils/__init__.py create mode 100644 src/core/utils/calendar.py create mode 100644 src/core/utils/dataclasses.py create mode 100644 src/core/utils/feature_flags.py create mode 100644 src/core/utils/http_utils.py create mode 100644 src/core/utils/quota_limit_ops.py create mode 100644 src/core/utils/service_retry.py create mode 100644 src/core/utils/settings_helper.py create mode 100644 src/core/views/__init__.py create mode 100644 src/core/views/auth/__init__.py create mode 100644 src/core/views/auth/create_account.py create mode 100644 src/core/views/auth/helpers.py create mode 100644 src/core/views/auth/login.py create mode 100644 src/core/views/auth/passwords/__init__.py create mode 100644 src/core/views/auth/passwords/generate.py create mode 100644 src/core/views/auth/passwords/set.py create mode 100644 src/core/views/auth/passwords/view.py create mode 100644 src/core/views/auth/urls.py create mode 100644 src/core/views/auth/verify.py create mode 100644 src/core/views/emails/__init__.py create mode 100644 src/core/views/emails/dashboard.py create mode 100644 src/core/views/emails/urls.py create mode 100644 src/core/views/modals/__init__.py create mode 100644 src/core/views/modals/open.py create mode 100644 src/core/views/other/__init__.py create mode 100644 src/core/views/other/errors.py create mode 100644 src/core/views/other/index.py create mode 100644 src/core/views/quotas/__init__.py create mode 100644 src/core/views/quotas/view.py create mode 100644 src/core/views/settings/__init__.py create mode 100644 src/core/views/settings/teams.py create mode 100644 src/core/views/settings/urls.py create mode 100644 src/core/views/settings/view.py create mode 100644 src/core/views/teams/__init__.py create mode 100644 src/core/views/teams/urls.py create mode 100644 src/makemigrations.py create mode 100644 src/settings.py create mode 100644 static/src/input.css create mode 100644 static/tailwind.config.js diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8085211 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: [strelix, treyww] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/1-bug_report.yml b/.github/ISSUE_TEMPLATE/1-bug_report.yml new file mode 100644 index 0000000..567096d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug_report.yml @@ -0,0 +1,37 @@ +name: 🐛 Report a bug +description: If you have found a bug on Strelix Core report it here! +title: "bug: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + + Hey there, thanks for creating a bug report, it's much appreciated! + > Note: This is strictly for **bug** reports for the **CORE** repository, issues regarding myfinances should be posted + on the [MyFinances Repo](https://github.com/Trey/MyFinances) + + Before creating the bug report, please make sure that you have done the following steps: + - [ ] Only post **one** problem *per* bug report + - [ ] Updated to the latest [official release](https://github.com/Strelix/Core/releases) + - [ ] Update your database with the latest migrations `py manage.py migrate` + + - type: input + id: version + validations: + required: true + attributes: + label: What version of Strelix-Core are you currently using? + description: You can view by using the command `git describe --tags` + placeholder: "v0.1.0" + - type: textarea + id: description + validations: + required: true + attributes: + label: Describe the bug + description: | + Please describe the bug in detail, make sure to list HOW you get the bug to come up (steps exactly what you follow, including the page path) + > To use code blocks, use ``` at the start and end of your code snippets. DO NOT USE SCREENSHOTS OF CODE. + + You may attach images, please upload directly using github \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/2-ideas.yml b/.github/ISSUE_TEMPLATE/2-ideas.yml new file mode 100644 index 0000000..79c730b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-ideas.yml @@ -0,0 +1,49 @@ +name: 💡 Suggest a new feature +description: If you have an idea for the core repository, suggest it here! +title: "idea: " +labels: ["idea", "idea: suggested"] +body: + - type: markdown + attributes: + value: | + + > Note: This is strictly for **feature requests**, issues setting up or bug reports be created with the "Report a Bug" form. + + Before creating the feature request, please make sure that you have done the following steps: + - [ ] Checked our existing [MyFinances feature requests](https://github.com/TreyWW/MyFinances/issues?q=is%3Aissue+label%3Aidea+) [Core feature requests](https://github.com/TreyWW/MyFinances/issues?q=is%3Aissue+label%3Aidea+) + - [ ] Updated to the latest [official release](https://github.com/Strelix/Core/releases) + + - type: dropdown + id: size + validations: + required: true + attributes: + label: What's the scope of this feature? + multiple: false + options: + - Small + - Medium + - Big feature + - type: textarea + id: problem + validations: + required: true + attributes: + label: Describe the problem + description: | + Please describe the problem needed to be solved in detail. + + You may upload images through github and display them here. + - type: textarea + id: solution + validations: + required: false + attributes: + label: Describe a possible solution + description: | + [OPTIONAL] Please describe a potential solution to the problem that we may be able to solve. + + Things you may want to address: + - Details of the technical implementation + - Tradeoffs made in design decisions + - Caveats and considerations for the future diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..bb45442 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: 💸 MyFinances + url: https://github.com/TreyWW/MyFinances/ + about: Want to go to our main application repository? MyFinances! + - name: 🔍 View Issues + url: https://github.com/Strelix/Core/issues?q=is%3Aissue + about: If you have found an issue (or a bug), try searching our issues tab before reporting it. + - name: ❓ Need some help + url: https://github.com/Strelix/Core/discussions/categories/q-a + about: If you have a question or are stuck setting up the project, open a Q&A discussion! +# - name: 📖 View our documentation +# url: https://docs.strelix.org/?utm_source=github_issue_create +# about: Check out our documentation to get started! We have lots of helpful info in there diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7fc7165 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + open-pull-requests-limit: 0 + schedule: + interval: "weekly" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: "/" + open-pull-requests-limit: 0 + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3d079f8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +## Description + + + +## Related Info +- Related Issue # +- Closes # diff --git a/.github/workflows/check_typos.yml b/.github/workflows/check_typos.yml new file mode 100644 index 0000000..6d036df --- /dev/null +++ b/.github/workflows/check_typos.yml @@ -0,0 +1,36 @@ +name: Check Typos +on: + push: + branches: + - main + pull_request: + +jobs: + check-typos: + strategy: + fail-fast: true + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.*" ] + + runs-on: ${{ matrix.os }} + + steps: + #---------------------------------------------- + # check-out repo and set-up python + #---------------------------------------------- + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + #---------------------------------------------- + # check grammar with typos + #---------------------------------------------- + - name: Check grammar with typos + run: | + pip3 install typos + typos \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..65429a8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,81 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + paths: + - 'src/**' + pull_request: + branches: [ "main" ] + paths: + - 'src/**' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..888f3ed --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,39 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable +# packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency review' +on: + pull_request: + branches: [ "main" ] + +# If using a dependency submission action in this workflow this permission will need to be set to: +# +# permissions: +# contents: write +# +# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api +permissions: + contents: read + # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option + pull-requests: write + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 + # Commonly enabled options, see hh for all available options. + with: + comment-summary-in-pr: always + # fail-on-severity: moderate + # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later + # retry-on-snapshot-warnings: true diff --git a/.github/workflows/docs.yml.disabled b/.github/workflows/docs.yml.disabled new file mode 100644 index 0000000..0acd48b --- /dev/null +++ b/.github/workflows/docs.yml.disabled @@ -0,0 +1,34 @@ +name: Publish Docs +on: + push: + branches: + - main + paths: + - docs/** + - .github/workflows/docs.yml +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mkdocs-material mkdocs-git-committers-plugin-2 + - run: mkdocs gh-deploy --force + env: + UMAMI_PAGE_URL: ${{ secrets.UMAMI_PAGE_URL }} + UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID }} diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml new file mode 100644 index 0000000..861f116 --- /dev/null +++ b/.github/workflows/publish_to_pypi.yml @@ -0,0 +1,49 @@ +name: Publish to PyPi +on: + push: + tags: + - v* + workflow_dispatch: + +jobs: +# test: +# runs-on: ${{ matrix.os }} +# strategy: +# fail-fast: false +# matrix: +# os: [ ubuntu-latest ] +# python-version: [ "3.10", "3.11", "3.12" ] +# django-version: [ 4, 5 ] +# steps: +# - uses: actions/checkout@v4 +# +# - uses: ./.github/actions/test +# with: +# python-version: ${{ matrix.python-version }} +# django-version: ${{ matrix.django-version }} + + release: + runs-on: ubuntu-latest +# needs: [test] + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: 3.11 + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build + run: python -m build + + - name: Check + run: twine check dist/* + + - name: Release + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/.github/workflows/run_black_linter.yml b/.github/workflows/run_black_linter.yml new file mode 100644 index 0000000..f9bcd9b --- /dev/null +++ b/.github/workflows/run_black_linter.yml @@ -0,0 +1,18 @@ +name: Black Linter (PY) + +on: + push: + branches: + - main + paths: + - '**.py' + pull_request: + paths: + - '**.py' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.1.6 + - uses: psf/black@stable diff --git a/.github/workflows/run_djlint.yml b/.github/workflows/run_djlint.yml new file mode 100644 index 0000000..0d7c02a --- /dev/null +++ b/.github/workflows/run_djlint.yml @@ -0,0 +1,55 @@ +name: djLinter (HTML) +on: + push: + branches: + - main + paths: + - 'frontend/templates/**.html' + pull_request: + paths: + - 'frontend/templates/**.html' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + #---------------------------------------------- + # check-out repo and set-up python + #---------------------------------------------- + - name: Check out repository + uses: actions/checkout@v4.1.6 + - name: Set up python + id: setup-python + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.12" + #---------------------------------------------- + # ----- install & configure poetry ----- + #---------------------------------------------- + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4.0.2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --only dev --no-interaction --no-root + #---------------------------------------------- + # add matrix specifics and run lints + #---------------------------------------------- + - name: Run Lint + run: | + source .venv/bin/activate + poetry run djlint ./frontend/templates/ --check --profile=django diff --git a/.github/workflows/run_mypy.yml b/.github/workflows/run_mypy.yml new file mode 100644 index 0000000..61aec08 --- /dev/null +++ b/.github/workflows/run_mypy.yml @@ -0,0 +1,47 @@ +name: Mypy +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + mypy: + name: mypy + runs-on: ubuntu-latest + env: + SECRET_KEY: "some!random!secret!key!use!online!generator!to!get" + URL: "127.0.0.1" + PROXY_IP: "localhost" + BRANCH: "debug" + DEBUG: "true" + DATABASE_TYPE: "sqlite3" + SITE_URL: "http://myfinances.example.com" + SITE_NAME: "myfinances" + steps: + - name: Clone repo + uses: actions/checkout@v4.1.6 + - name: Set up python + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.12" + - name: Install and configure Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4.0.2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + - name: Run mypy checks + run: | + source .venv/bin/activate + mypy . -i diff --git a/.github/workflows/stale_prs.yml b/.github/workflows/stale_prs.yml new file mode 100644 index 0000000..f691202 --- /dev/null +++ b/.github/workflows/stale_prs.yml @@ -0,0 +1,21 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +permissions: + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: 'This issue is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 7 days.' + days-before-pr-stale: 14 + days-before-pr-close: 7 + exempt-pr-labels: awaiting-changes,on-hold,no-stale + days-before-issue-close: -1 + days-before-issue-stale: -1 + exempt-assignees: treyww diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1facc0a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,83 @@ +name: Backend Tests +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + test: + name: test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.10", "3.11", "3.12" ] + steps: + - name: Clone repo + uses: actions/checkout@v4.1.6 + - name: Set up python + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + - name: Install and configure Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4.0.2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + - name: Install latest setuptools + run: | + source .venv/bin/activate + pip install setuptools + - name: Install latest Django + run: | + source .venv/bin/activate + pip install Django + - name: Install dependencies and build frontend + run: | + npm ci + npm run tailwind-build + npm run webpack-build + + if [ -f frontend/static/src/output.css ]; then + echo "output.css is created" + else + echo "output.css is not created" + exit 1 + fi + if [ -d frontend/static/js/c/ ]; then + echo "webpack has been built" + else + echo "frontend/static/js/c/ was not created from webpack build" + exit 1 + fi + if [ -f webpack-stats.json ]; then + echo "webpack has built webpack-stats to work with chunks" + else + echo "webpack-stats was not built so cannot work with chunks" + exit 1 + fi + - name: Run tests + env: + SECRET_KEY: "some!random!secret!key!use!online!generator!to!get" + URL: "127.0.0.1" + PROXY_IP: "localhost" + BRANCH: "debug" + DEBUG: "true" + DATABASE_TYPE: "sqlite3" + SITE_URL: "http://myfinances.example.com" + SITE_NAME: "myfinances" + run: | + source .venv/bin/activate + python3 manage.py collectstatic --no-input + python3 manage.py test --parallel diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85455bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,153 @@ +# Byte-compiled / optimized / DLL files +*/__pycache__/* +*.py[cod] +*.pyc +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +*.env +environment.yml + +!.env.sample + +.venv +env/ +venv/ +ENV/ +ENV_DIR/ +env.bak/ +venv.bak/ +media/* +.idea + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# VS Code +.vscode + +# Node +node_modules + +# Generated fronted files +frontend/static/src/output.css +frontend/static/js/bundle.js +frontend/static/js/bundle.js.map +frontend/static/js/c/* + +# Webpack +webpack-stats.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6937810 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Strelix Core + diff --git a/makemigrations.py b/makemigrations.py new file mode 100644 index 0000000..2f82afe --- /dev/null +++ b/makemigrations.py @@ -0,0 +1,8 @@ +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") + from django.core.management import execute_from_command_line + args = sys.argv + ["makemigrations", "core"] + execute_from_command_line(args) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3418797 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1308 @@ +{ + "name": "core", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "daisyui": "^4.12.22", + "tailwindcss": "^3.4.17" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.22", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.22.tgz", + "integrity": "sha512-HDLWbmTnXxhE1MrMgSWjVgdRt+bVYHvfNbW3GTsyIokRSqTHonUTrxV3RhpPDjGIWaHt+ELtDCTYCtUFgL2/Nw==", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3ae093 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "strelix-core", + "version": "0.0.1", + "main": "static/tailwind.config.js", + "style": "static/src/input.css", + "dependencies": { + "daisyui": "^4.12.22", + "tailwindcss": "^3.4.17" + } +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..5ad74a7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3494 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "autopep8" +version = "2.3.1" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +optional = false +python-versions = ">=3.8" +files = [ + {file = "autopep8-2.3.1-py2.py3-none-any.whl", hash = "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d"}, + {file = "autopep8-2.3.1.tar.gz", hash = "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda"}, +] + +[package.dependencies] +pycodestyle = ">=2.12.0" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bleach" +version = "6.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +optional = false +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + +[[package]] +name = "boto3" +version = "1.34.76" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "boto3-1.34.76-py3-none-any.whl", hash = "sha256:530a4cea3d40a6bd2f15a368ea395beef1ea6dff4491823bc48bd20c7d4da655"}, + {file = "boto3-1.34.76.tar.gz", hash = "sha256:8c598382e8fb61cfa8f75056197e9b509eb52039ebc291af3b1096241ba2542c"}, +] + +[package.dependencies] +botocore = ">=1.34.76,<1.35.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.10.0,<0.11.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "boto3-stubs" +version = "1.35.85" +description = "Type annotations for boto3 1.35.85 generated with mypy-boto3-builder 8.6.4" +optional = false +python-versions = ">=3.8" +files = [ + {file = "boto3_stubs-1.35.85-py3-none-any.whl", hash = "sha256:c3c1709603cb9d0fba4667b8408847f05b8f0b92bb74e88e0e97571cb6dd7745"}, + {file = "boto3_stubs-1.35.85.tar.gz", hash = "sha256:c949abdba605dec649cfceab95f573c8fbce575ed23d8522e965b9eb6da4eeba"}, +] + +[package.dependencies] +botocore-stubs = "*" +mypy-boto3-dynamodb = {version = ">=1.35.0,<1.36.0", optional = true, markers = "extra == \"dynamodb\""} +mypy-boto3-events = {version = ">=1.35.0,<1.36.0", optional = true, markers = "extra == \"events\""} +mypy-boto3-iam = {version = ">=1.35.0,<1.36.0", optional = true, markers = "extra == \"iam\""} +mypy-boto3-scheduler = {version = ">=1.35.0,<1.36.0", optional = true, markers = "extra == \"scheduler\""} +mypy-boto3-sesv2 = {version = ">=1.35.0,<1.36.0", optional = true, markers = "extra == \"sesv2\""} +mypy-boto3-stepfunctions = {version = ">=1.35.0,<1.36.0", optional = true, markers = "extra == \"stepfunctions\""} +types-s3transfer = "*" +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[package.extras] +accessanalyzer = ["mypy-boto3-accessanalyzer (>=1.35.0,<1.36.0)"] +account = ["mypy-boto3-account (>=1.35.0,<1.36.0)"] +acm = ["mypy-boto3-acm (>=1.35.0,<1.36.0)"] +acm-pca = ["mypy-boto3-acm-pca (>=1.35.0,<1.36.0)"] +all = ["mypy-boto3-accessanalyzer (>=1.35.0,<1.36.0)", "mypy-boto3-account (>=1.35.0,<1.36.0)", "mypy-boto3-acm (>=1.35.0,<1.36.0)", "mypy-boto3-acm-pca (>=1.35.0,<1.36.0)", "mypy-boto3-amp (>=1.35.0,<1.36.0)", "mypy-boto3-amplify (>=1.35.0,<1.36.0)", "mypy-boto3-amplifybackend (>=1.35.0,<1.36.0)", "mypy-boto3-amplifyuibuilder (>=1.35.0,<1.36.0)", "mypy-boto3-apigateway (>=1.35.0,<1.36.0)", "mypy-boto3-apigatewaymanagementapi (>=1.35.0,<1.36.0)", "mypy-boto3-apigatewayv2 (>=1.35.0,<1.36.0)", "mypy-boto3-appconfig (>=1.35.0,<1.36.0)", "mypy-boto3-appconfigdata (>=1.35.0,<1.36.0)", "mypy-boto3-appfabric (>=1.35.0,<1.36.0)", "mypy-boto3-appflow (>=1.35.0,<1.36.0)", "mypy-boto3-appintegrations (>=1.35.0,<1.36.0)", "mypy-boto3-application-autoscaling (>=1.35.0,<1.36.0)", "mypy-boto3-application-insights (>=1.35.0,<1.36.0)", "mypy-boto3-application-signals (>=1.35.0,<1.36.0)", "mypy-boto3-applicationcostprofiler (>=1.35.0,<1.36.0)", "mypy-boto3-appmesh (>=1.35.0,<1.36.0)", "mypy-boto3-apprunner (>=1.35.0,<1.36.0)", "mypy-boto3-appstream (>=1.35.0,<1.36.0)", "mypy-boto3-appsync (>=1.35.0,<1.36.0)", "mypy-boto3-apptest (>=1.35.0,<1.36.0)", "mypy-boto3-arc-zonal-shift (>=1.35.0,<1.36.0)", "mypy-boto3-artifact (>=1.35.0,<1.36.0)", "mypy-boto3-athena (>=1.35.0,<1.36.0)", "mypy-boto3-auditmanager (>=1.35.0,<1.36.0)", "mypy-boto3-autoscaling (>=1.35.0,<1.36.0)", "mypy-boto3-autoscaling-plans (>=1.35.0,<1.36.0)", "mypy-boto3-b2bi (>=1.35.0,<1.36.0)", "mypy-boto3-backup (>=1.35.0,<1.36.0)", "mypy-boto3-backup-gateway (>=1.35.0,<1.36.0)", "mypy-boto3-backupsearch (>=1.35.0,<1.36.0)", "mypy-boto3-batch (>=1.35.0,<1.36.0)", "mypy-boto3-bcm-data-exports (>=1.35.0,<1.36.0)", "mypy-boto3-bcm-pricing-calculator (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-agent (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-agent-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-data-automation (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-data-automation-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-bedrock-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-billing (>=1.35.0,<1.36.0)", "mypy-boto3-billingconductor (>=1.35.0,<1.36.0)", "mypy-boto3-braket (>=1.35.0,<1.36.0)", "mypy-boto3-budgets (>=1.35.0,<1.36.0)", "mypy-boto3-ce (>=1.35.0,<1.36.0)", "mypy-boto3-chatbot (>=1.35.0,<1.36.0)", "mypy-boto3-chime (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-identity (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-media-pipelines (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-meetings (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-messaging (>=1.35.0,<1.36.0)", "mypy-boto3-chime-sdk-voice (>=1.35.0,<1.36.0)", "mypy-boto3-cleanrooms (>=1.35.0,<1.36.0)", "mypy-boto3-cleanroomsml (>=1.35.0,<1.36.0)", "mypy-boto3-cloud9 (>=1.35.0,<1.36.0)", "mypy-boto3-cloudcontrol (>=1.35.0,<1.36.0)", "mypy-boto3-clouddirectory (>=1.35.0,<1.36.0)", "mypy-boto3-cloudformation (>=1.35.0,<1.36.0)", "mypy-boto3-cloudfront (>=1.35.0,<1.36.0)", "mypy-boto3-cloudfront-keyvaluestore (>=1.35.0,<1.36.0)", "mypy-boto3-cloudhsm (>=1.35.0,<1.36.0)", "mypy-boto3-cloudhsmv2 (>=1.35.0,<1.36.0)", "mypy-boto3-cloudsearch (>=1.35.0,<1.36.0)", "mypy-boto3-cloudsearchdomain (>=1.35.0,<1.36.0)", "mypy-boto3-cloudtrail (>=1.35.0,<1.36.0)", "mypy-boto3-cloudtrail-data (>=1.35.0,<1.36.0)", "mypy-boto3-cloudwatch (>=1.35.0,<1.36.0)", "mypy-boto3-codeartifact (>=1.35.0,<1.36.0)", "mypy-boto3-codebuild (>=1.35.0,<1.36.0)", "mypy-boto3-codecatalyst (>=1.35.0,<1.36.0)", "mypy-boto3-codecommit (>=1.35.0,<1.36.0)", "mypy-boto3-codeconnections (>=1.35.0,<1.36.0)", "mypy-boto3-codedeploy (>=1.35.0,<1.36.0)", "mypy-boto3-codeguru-reviewer (>=1.35.0,<1.36.0)", "mypy-boto3-codeguru-security (>=1.35.0,<1.36.0)", "mypy-boto3-codeguruprofiler (>=1.35.0,<1.36.0)", "mypy-boto3-codepipeline (>=1.35.0,<1.36.0)", "mypy-boto3-codestar-connections (>=1.35.0,<1.36.0)", "mypy-boto3-codestar-notifications (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-identity (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-idp (>=1.35.0,<1.36.0)", "mypy-boto3-cognito-sync (>=1.35.0,<1.36.0)", "mypy-boto3-comprehend (>=1.35.0,<1.36.0)", "mypy-boto3-comprehendmedical (>=1.35.0,<1.36.0)", "mypy-boto3-compute-optimizer (>=1.35.0,<1.36.0)", "mypy-boto3-config (>=1.35.0,<1.36.0)", "mypy-boto3-connect (>=1.35.0,<1.36.0)", "mypy-boto3-connect-contact-lens (>=1.35.0,<1.36.0)", "mypy-boto3-connectcampaigns (>=1.35.0,<1.36.0)", "mypy-boto3-connectcampaignsv2 (>=1.35.0,<1.36.0)", "mypy-boto3-connectcases (>=1.35.0,<1.36.0)", "mypy-boto3-connectparticipant (>=1.35.0,<1.36.0)", "mypy-boto3-controlcatalog (>=1.35.0,<1.36.0)", "mypy-boto3-controltower (>=1.35.0,<1.36.0)", "mypy-boto3-cost-optimization-hub (>=1.35.0,<1.36.0)", "mypy-boto3-cur (>=1.35.0,<1.36.0)", "mypy-boto3-customer-profiles (>=1.35.0,<1.36.0)", "mypy-boto3-databrew (>=1.35.0,<1.36.0)", "mypy-boto3-dataexchange (>=1.35.0,<1.36.0)", "mypy-boto3-datapipeline (>=1.35.0,<1.36.0)", "mypy-boto3-datasync (>=1.35.0,<1.36.0)", "mypy-boto3-datazone (>=1.35.0,<1.36.0)", "mypy-boto3-dax (>=1.35.0,<1.36.0)", "mypy-boto3-deadline (>=1.35.0,<1.36.0)", "mypy-boto3-detective (>=1.35.0,<1.36.0)", "mypy-boto3-devicefarm (>=1.35.0,<1.36.0)", "mypy-boto3-devops-guru (>=1.35.0,<1.36.0)", "mypy-boto3-directconnect (>=1.35.0,<1.36.0)", "mypy-boto3-discovery (>=1.35.0,<1.36.0)", "mypy-boto3-dlm (>=1.35.0,<1.36.0)", "mypy-boto3-dms (>=1.35.0,<1.36.0)", "mypy-boto3-docdb (>=1.35.0,<1.36.0)", "mypy-boto3-docdb-elastic (>=1.35.0,<1.36.0)", "mypy-boto3-drs (>=1.35.0,<1.36.0)", "mypy-boto3-ds (>=1.35.0,<1.36.0)", "mypy-boto3-ds-data (>=1.35.0,<1.36.0)", "mypy-boto3-dsql (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodb (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodbstreams (>=1.35.0,<1.36.0)", "mypy-boto3-ebs (>=1.35.0,<1.36.0)", "mypy-boto3-ec2 (>=1.35.0,<1.36.0)", "mypy-boto3-ec2-instance-connect (>=1.35.0,<1.36.0)", "mypy-boto3-ecr (>=1.35.0,<1.36.0)", "mypy-boto3-ecr-public (>=1.35.0,<1.36.0)", "mypy-boto3-ecs (>=1.35.0,<1.36.0)", "mypy-boto3-efs (>=1.35.0,<1.36.0)", "mypy-boto3-eks (>=1.35.0,<1.36.0)", "mypy-boto3-eks-auth (>=1.35.0,<1.36.0)", "mypy-boto3-elastic-inference (>=1.35.0,<1.36.0)", "mypy-boto3-elasticache (>=1.35.0,<1.36.0)", "mypy-boto3-elasticbeanstalk (>=1.35.0,<1.36.0)", "mypy-boto3-elastictranscoder (>=1.35.0,<1.36.0)", "mypy-boto3-elb (>=1.35.0,<1.36.0)", "mypy-boto3-elbv2 (>=1.35.0,<1.36.0)", "mypy-boto3-emr (>=1.35.0,<1.36.0)", "mypy-boto3-emr-containers (>=1.35.0,<1.36.0)", "mypy-boto3-emr-serverless (>=1.35.0,<1.36.0)", "mypy-boto3-entityresolution (>=1.35.0,<1.36.0)", "mypy-boto3-es (>=1.35.0,<1.36.0)", "mypy-boto3-events (>=1.35.0,<1.36.0)", "mypy-boto3-evidently (>=1.35.0,<1.36.0)", "mypy-boto3-finspace (>=1.35.0,<1.36.0)", "mypy-boto3-finspace-data (>=1.35.0,<1.36.0)", "mypy-boto3-firehose (>=1.35.0,<1.36.0)", "mypy-boto3-fis (>=1.35.0,<1.36.0)", "mypy-boto3-fms (>=1.35.0,<1.36.0)", "mypy-boto3-forecast (>=1.35.0,<1.36.0)", "mypy-boto3-forecastquery (>=1.35.0,<1.36.0)", "mypy-boto3-frauddetector (>=1.35.0,<1.36.0)", "mypy-boto3-freetier (>=1.35.0,<1.36.0)", "mypy-boto3-fsx (>=1.35.0,<1.36.0)", "mypy-boto3-gamelift (>=1.35.0,<1.36.0)", "mypy-boto3-geo-maps (>=1.35.0,<1.36.0)", "mypy-boto3-geo-places (>=1.35.0,<1.36.0)", "mypy-boto3-geo-routes (>=1.35.0,<1.36.0)", "mypy-boto3-glacier (>=1.35.0,<1.36.0)", "mypy-boto3-globalaccelerator (>=1.35.0,<1.36.0)", "mypy-boto3-glue (>=1.35.0,<1.36.0)", "mypy-boto3-grafana (>=1.35.0,<1.36.0)", "mypy-boto3-greengrass (>=1.35.0,<1.36.0)", "mypy-boto3-greengrassv2 (>=1.35.0,<1.36.0)", "mypy-boto3-groundstation (>=1.35.0,<1.36.0)", "mypy-boto3-guardduty (>=1.35.0,<1.36.0)", "mypy-boto3-health (>=1.35.0,<1.36.0)", "mypy-boto3-healthlake (>=1.35.0,<1.36.0)", "mypy-boto3-iam (>=1.35.0,<1.36.0)", "mypy-boto3-identitystore (>=1.35.0,<1.36.0)", "mypy-boto3-imagebuilder (>=1.35.0,<1.36.0)", "mypy-boto3-importexport (>=1.35.0,<1.36.0)", "mypy-boto3-inspector (>=1.35.0,<1.36.0)", "mypy-boto3-inspector-scan (>=1.35.0,<1.36.0)", "mypy-boto3-inspector2 (>=1.35.0,<1.36.0)", "mypy-boto3-internetmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-invoicing (>=1.35.0,<1.36.0)", "mypy-boto3-iot (>=1.35.0,<1.36.0)", "mypy-boto3-iot-data (>=1.35.0,<1.36.0)", "mypy-boto3-iot-jobs-data (>=1.35.0,<1.36.0)", "mypy-boto3-iot1click-devices (>=1.35.0,<1.36.0)", "mypy-boto3-iot1click-projects (>=1.35.0,<1.36.0)", "mypy-boto3-iotanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-iotdeviceadvisor (>=1.35.0,<1.36.0)", "mypy-boto3-iotevents (>=1.35.0,<1.36.0)", "mypy-boto3-iotevents-data (>=1.35.0,<1.36.0)", "mypy-boto3-iotfleethub (>=1.35.0,<1.36.0)", "mypy-boto3-iotfleetwise (>=1.35.0,<1.36.0)", "mypy-boto3-iotsecuretunneling (>=1.35.0,<1.36.0)", "mypy-boto3-iotsitewise (>=1.35.0,<1.36.0)", "mypy-boto3-iotthingsgraph (>=1.35.0,<1.36.0)", "mypy-boto3-iottwinmaker (>=1.35.0,<1.36.0)", "mypy-boto3-iotwireless (>=1.35.0,<1.36.0)", "mypy-boto3-ivs (>=1.35.0,<1.36.0)", "mypy-boto3-ivs-realtime (>=1.35.0,<1.36.0)", "mypy-boto3-ivschat (>=1.35.0,<1.36.0)", "mypy-boto3-kafka (>=1.35.0,<1.36.0)", "mypy-boto3-kafkaconnect (>=1.35.0,<1.36.0)", "mypy-boto3-kendra (>=1.35.0,<1.36.0)", "mypy-boto3-kendra-ranking (>=1.35.0,<1.36.0)", "mypy-boto3-keyspaces (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-archived-media (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-media (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-signaling (>=1.35.0,<1.36.0)", "mypy-boto3-kinesis-video-webrtc-storage (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisanalyticsv2 (>=1.35.0,<1.36.0)", "mypy-boto3-kinesisvideo (>=1.35.0,<1.36.0)", "mypy-boto3-kms (>=1.35.0,<1.36.0)", "mypy-boto3-lakeformation (>=1.35.0,<1.36.0)", "mypy-boto3-lambda (>=1.35.0,<1.36.0)", "mypy-boto3-launch-wizard (>=1.35.0,<1.36.0)", "mypy-boto3-lex-models (>=1.35.0,<1.36.0)", "mypy-boto3-lex-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-lexv2-models (>=1.35.0,<1.36.0)", "mypy-boto3-lexv2-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager-linux-subscriptions (>=1.35.0,<1.36.0)", "mypy-boto3-license-manager-user-subscriptions (>=1.35.0,<1.36.0)", "mypy-boto3-lightsail (>=1.35.0,<1.36.0)", "mypy-boto3-location (>=1.35.0,<1.36.0)", "mypy-boto3-logs (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutequipment (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutmetrics (>=1.35.0,<1.36.0)", "mypy-boto3-lookoutvision (>=1.35.0,<1.36.0)", "mypy-boto3-m2 (>=1.35.0,<1.36.0)", "mypy-boto3-machinelearning (>=1.35.0,<1.36.0)", "mypy-boto3-macie2 (>=1.35.0,<1.36.0)", "mypy-boto3-mailmanager (>=1.35.0,<1.36.0)", "mypy-boto3-managedblockchain (>=1.35.0,<1.36.0)", "mypy-boto3-managedblockchain-query (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-agreement (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-catalog (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-deployment (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-entitlement (>=1.35.0,<1.36.0)", "mypy-boto3-marketplace-reporting (>=1.35.0,<1.36.0)", "mypy-boto3-marketplacecommerceanalytics (>=1.35.0,<1.36.0)", "mypy-boto3-mediaconnect (>=1.35.0,<1.36.0)", "mypy-boto3-mediaconvert (>=1.35.0,<1.36.0)", "mypy-boto3-medialive (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackage (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackage-vod (>=1.35.0,<1.36.0)", "mypy-boto3-mediapackagev2 (>=1.35.0,<1.36.0)", "mypy-boto3-mediastore (>=1.35.0,<1.36.0)", "mypy-boto3-mediastore-data (>=1.35.0,<1.36.0)", "mypy-boto3-mediatailor (>=1.35.0,<1.36.0)", "mypy-boto3-medical-imaging (>=1.35.0,<1.36.0)", "mypy-boto3-memorydb (>=1.35.0,<1.36.0)", "mypy-boto3-meteringmarketplace (>=1.35.0,<1.36.0)", "mypy-boto3-mgh (>=1.35.0,<1.36.0)", "mypy-boto3-mgn (>=1.35.0,<1.36.0)", "mypy-boto3-migration-hub-refactor-spaces (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhub-config (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhuborchestrator (>=1.35.0,<1.36.0)", "mypy-boto3-migrationhubstrategy (>=1.35.0,<1.36.0)", "mypy-boto3-mq (>=1.35.0,<1.36.0)", "mypy-boto3-mturk (>=1.35.0,<1.36.0)", "mypy-boto3-mwaa (>=1.35.0,<1.36.0)", "mypy-boto3-neptune (>=1.35.0,<1.36.0)", "mypy-boto3-neptune-graph (>=1.35.0,<1.36.0)", "mypy-boto3-neptunedata (>=1.35.0,<1.36.0)", "mypy-boto3-network-firewall (>=1.35.0,<1.36.0)", "mypy-boto3-networkflowmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-networkmanager (>=1.35.0,<1.36.0)", "mypy-boto3-networkmonitor (>=1.35.0,<1.36.0)", "mypy-boto3-notifications (>=1.35.0,<1.36.0)", "mypy-boto3-notificationscontacts (>=1.35.0,<1.36.0)", "mypy-boto3-oam (>=1.35.0,<1.36.0)", "mypy-boto3-observabilityadmin (>=1.35.0,<1.36.0)", "mypy-boto3-omics (>=1.35.0,<1.36.0)", "mypy-boto3-opensearch (>=1.35.0,<1.36.0)", "mypy-boto3-opensearchserverless (>=1.35.0,<1.36.0)", "mypy-boto3-opsworks (>=1.35.0,<1.36.0)", "mypy-boto3-opsworkscm (>=1.35.0,<1.36.0)", "mypy-boto3-organizations (>=1.35.0,<1.36.0)", "mypy-boto3-osis (>=1.35.0,<1.36.0)", "mypy-boto3-outposts (>=1.35.0,<1.36.0)", "mypy-boto3-panorama (>=1.35.0,<1.36.0)", "mypy-boto3-partnercentral-selling (>=1.35.0,<1.36.0)", "mypy-boto3-payment-cryptography (>=1.35.0,<1.36.0)", "mypy-boto3-payment-cryptography-data (>=1.35.0,<1.36.0)", "mypy-boto3-pca-connector-ad (>=1.35.0,<1.36.0)", "mypy-boto3-pca-connector-scep (>=1.35.0,<1.36.0)", "mypy-boto3-pcs (>=1.35.0,<1.36.0)", "mypy-boto3-personalize (>=1.35.0,<1.36.0)", "mypy-boto3-personalize-events (>=1.35.0,<1.36.0)", "mypy-boto3-personalize-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-pi (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-email (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-sms-voice (>=1.35.0,<1.36.0)", "mypy-boto3-pinpoint-sms-voice-v2 (>=1.35.0,<1.36.0)", "mypy-boto3-pipes (>=1.35.0,<1.36.0)", "mypy-boto3-polly (>=1.35.0,<1.36.0)", "mypy-boto3-pricing (>=1.35.0,<1.36.0)", "mypy-boto3-privatenetworks (>=1.35.0,<1.36.0)", "mypy-boto3-proton (>=1.35.0,<1.36.0)", "mypy-boto3-qapps (>=1.35.0,<1.36.0)", "mypy-boto3-qbusiness (>=1.35.0,<1.36.0)", "mypy-boto3-qconnect (>=1.35.0,<1.36.0)", "mypy-boto3-qldb (>=1.35.0,<1.36.0)", "mypy-boto3-qldb-session (>=1.35.0,<1.36.0)", "mypy-boto3-quicksight (>=1.35.0,<1.36.0)", "mypy-boto3-ram (>=1.35.0,<1.36.0)", "mypy-boto3-rbin (>=1.35.0,<1.36.0)", "mypy-boto3-rds (>=1.35.0,<1.36.0)", "mypy-boto3-rds-data (>=1.35.0,<1.36.0)", "mypy-boto3-redshift (>=1.35.0,<1.36.0)", "mypy-boto3-redshift-data (>=1.35.0,<1.36.0)", "mypy-boto3-redshift-serverless (>=1.35.0,<1.36.0)", "mypy-boto3-rekognition (>=1.35.0,<1.36.0)", "mypy-boto3-repostspace (>=1.35.0,<1.36.0)", "mypy-boto3-resiliencehub (>=1.35.0,<1.36.0)", "mypy-boto3-resource-explorer-2 (>=1.35.0,<1.36.0)", "mypy-boto3-resource-groups (>=1.35.0,<1.36.0)", "mypy-boto3-resourcegroupstaggingapi (>=1.35.0,<1.36.0)", "mypy-boto3-robomaker (>=1.35.0,<1.36.0)", "mypy-boto3-rolesanywhere (>=1.35.0,<1.36.0)", "mypy-boto3-route53 (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-cluster (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-control-config (>=1.35.0,<1.36.0)", "mypy-boto3-route53-recovery-readiness (>=1.35.0,<1.36.0)", "mypy-boto3-route53domains (>=1.35.0,<1.36.0)", "mypy-boto3-route53profiles (>=1.35.0,<1.36.0)", "mypy-boto3-route53resolver (>=1.35.0,<1.36.0)", "mypy-boto3-rum (>=1.35.0,<1.36.0)", "mypy-boto3-s3 (>=1.35.0,<1.36.0)", "mypy-boto3-s3control (>=1.35.0,<1.36.0)", "mypy-boto3-s3outposts (>=1.35.0,<1.36.0)", "mypy-boto3-s3tables (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-a2i-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-edge (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-featurestore-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-geospatial (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-metrics (>=1.35.0,<1.36.0)", "mypy-boto3-sagemaker-runtime (>=1.35.0,<1.36.0)", "mypy-boto3-savingsplans (>=1.35.0,<1.36.0)", "mypy-boto3-scheduler (>=1.35.0,<1.36.0)", "mypy-boto3-schemas (>=1.35.0,<1.36.0)", "mypy-boto3-sdb (>=1.35.0,<1.36.0)", "mypy-boto3-secretsmanager (>=1.35.0,<1.36.0)", "mypy-boto3-security-ir (>=1.35.0,<1.36.0)", "mypy-boto3-securityhub (>=1.35.0,<1.36.0)", "mypy-boto3-securitylake (>=1.35.0,<1.36.0)", "mypy-boto3-serverlessrepo (>=1.35.0,<1.36.0)", "mypy-boto3-service-quotas (>=1.35.0,<1.36.0)", "mypy-boto3-servicecatalog (>=1.35.0,<1.36.0)", "mypy-boto3-servicecatalog-appregistry (>=1.35.0,<1.36.0)", "mypy-boto3-servicediscovery (>=1.35.0,<1.36.0)", "mypy-boto3-ses (>=1.35.0,<1.36.0)", "mypy-boto3-sesv2 (>=1.35.0,<1.36.0)", "mypy-boto3-shield (>=1.35.0,<1.36.0)", "mypy-boto3-signer (>=1.35.0,<1.36.0)", "mypy-boto3-simspaceweaver (>=1.35.0,<1.36.0)", "mypy-boto3-sms (>=1.35.0,<1.36.0)", "mypy-boto3-sms-voice (>=1.35.0,<1.36.0)", "mypy-boto3-snow-device-management (>=1.35.0,<1.36.0)", "mypy-boto3-snowball (>=1.35.0,<1.36.0)", "mypy-boto3-sns (>=1.35.0,<1.36.0)", "mypy-boto3-socialmessaging (>=1.35.0,<1.36.0)", "mypy-boto3-sqs (>=1.35.0,<1.36.0)", "mypy-boto3-ssm (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-contacts (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-incidents (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-quicksetup (>=1.35.0,<1.36.0)", "mypy-boto3-ssm-sap (>=1.35.0,<1.36.0)", "mypy-boto3-sso (>=1.35.0,<1.36.0)", "mypy-boto3-sso-admin (>=1.35.0,<1.36.0)", "mypy-boto3-sso-oidc (>=1.35.0,<1.36.0)", "mypy-boto3-stepfunctions (>=1.35.0,<1.36.0)", "mypy-boto3-storagegateway (>=1.35.0,<1.36.0)", "mypy-boto3-sts (>=1.35.0,<1.36.0)", "mypy-boto3-supplychain (>=1.35.0,<1.36.0)", "mypy-boto3-support (>=1.35.0,<1.36.0)", "mypy-boto3-support-app (>=1.35.0,<1.36.0)", "mypy-boto3-swf (>=1.35.0,<1.36.0)", "mypy-boto3-synthetics (>=1.35.0,<1.36.0)", "mypy-boto3-taxsettings (>=1.35.0,<1.36.0)", "mypy-boto3-textract (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-influxdb (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-query (>=1.35.0,<1.36.0)", "mypy-boto3-timestream-write (>=1.35.0,<1.36.0)", "mypy-boto3-tnb (>=1.35.0,<1.36.0)", "mypy-boto3-transcribe (>=1.35.0,<1.36.0)", "mypy-boto3-transfer (>=1.35.0,<1.36.0)", "mypy-boto3-translate (>=1.35.0,<1.36.0)", "mypy-boto3-trustedadvisor (>=1.35.0,<1.36.0)", "mypy-boto3-verifiedpermissions (>=1.35.0,<1.36.0)", "mypy-boto3-voice-id (>=1.35.0,<1.36.0)", "mypy-boto3-vpc-lattice (>=1.35.0,<1.36.0)", "mypy-boto3-waf (>=1.35.0,<1.36.0)", "mypy-boto3-waf-regional (>=1.35.0,<1.36.0)", "mypy-boto3-wafv2 (>=1.35.0,<1.36.0)", "mypy-boto3-wellarchitected (>=1.35.0,<1.36.0)", "mypy-boto3-wisdom (>=1.35.0,<1.36.0)", "mypy-boto3-workdocs (>=1.35.0,<1.36.0)", "mypy-boto3-workmail (>=1.35.0,<1.36.0)", "mypy-boto3-workmailmessageflow (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces-thin-client (>=1.35.0,<1.36.0)", "mypy-boto3-workspaces-web (>=1.35.0,<1.36.0)", "mypy-boto3-xray (>=1.35.0,<1.36.0)"] +amp = ["mypy-boto3-amp (>=1.35.0,<1.36.0)"] +amplify = ["mypy-boto3-amplify (>=1.35.0,<1.36.0)"] +amplifybackend = ["mypy-boto3-amplifybackend (>=1.35.0,<1.36.0)"] +amplifyuibuilder = ["mypy-boto3-amplifyuibuilder (>=1.35.0,<1.36.0)"] +apigateway = ["mypy-boto3-apigateway (>=1.35.0,<1.36.0)"] +apigatewaymanagementapi = ["mypy-boto3-apigatewaymanagementapi (>=1.35.0,<1.36.0)"] +apigatewayv2 = ["mypy-boto3-apigatewayv2 (>=1.35.0,<1.36.0)"] +appconfig = ["mypy-boto3-appconfig (>=1.35.0,<1.36.0)"] +appconfigdata = ["mypy-boto3-appconfigdata (>=1.35.0,<1.36.0)"] +appfabric = ["mypy-boto3-appfabric (>=1.35.0,<1.36.0)"] +appflow = ["mypy-boto3-appflow (>=1.35.0,<1.36.0)"] +appintegrations = ["mypy-boto3-appintegrations (>=1.35.0,<1.36.0)"] +application-autoscaling = ["mypy-boto3-application-autoscaling (>=1.35.0,<1.36.0)"] +application-insights = ["mypy-boto3-application-insights (>=1.35.0,<1.36.0)"] +application-signals = ["mypy-boto3-application-signals (>=1.35.0,<1.36.0)"] +applicationcostprofiler = ["mypy-boto3-applicationcostprofiler (>=1.35.0,<1.36.0)"] +appmesh = ["mypy-boto3-appmesh (>=1.35.0,<1.36.0)"] +apprunner = ["mypy-boto3-apprunner (>=1.35.0,<1.36.0)"] +appstream = ["mypy-boto3-appstream (>=1.35.0,<1.36.0)"] +appsync = ["mypy-boto3-appsync (>=1.35.0,<1.36.0)"] +apptest = ["mypy-boto3-apptest (>=1.35.0,<1.36.0)"] +arc-zonal-shift = ["mypy-boto3-arc-zonal-shift (>=1.35.0,<1.36.0)"] +artifact = ["mypy-boto3-artifact (>=1.35.0,<1.36.0)"] +athena = ["mypy-boto3-athena (>=1.35.0,<1.36.0)"] +auditmanager = ["mypy-boto3-auditmanager (>=1.35.0,<1.36.0)"] +autoscaling = ["mypy-boto3-autoscaling (>=1.35.0,<1.36.0)"] +autoscaling-plans = ["mypy-boto3-autoscaling-plans (>=1.35.0,<1.36.0)"] +b2bi = ["mypy-boto3-b2bi (>=1.35.0,<1.36.0)"] +backup = ["mypy-boto3-backup (>=1.35.0,<1.36.0)"] +backup-gateway = ["mypy-boto3-backup-gateway (>=1.35.0,<1.36.0)"] +backupsearch = ["mypy-boto3-backupsearch (>=1.35.0,<1.36.0)"] +batch = ["mypy-boto3-batch (>=1.35.0,<1.36.0)"] +bcm-data-exports = ["mypy-boto3-bcm-data-exports (>=1.35.0,<1.36.0)"] +bcm-pricing-calculator = ["mypy-boto3-bcm-pricing-calculator (>=1.35.0,<1.36.0)"] +bedrock = ["mypy-boto3-bedrock (>=1.35.0,<1.36.0)"] +bedrock-agent = ["mypy-boto3-bedrock-agent (>=1.35.0,<1.36.0)"] +bedrock-agent-runtime = ["mypy-boto3-bedrock-agent-runtime (>=1.35.0,<1.36.0)"] +bedrock-data-automation = ["mypy-boto3-bedrock-data-automation (>=1.35.0,<1.36.0)"] +bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (>=1.35.0,<1.36.0)"] +bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.35.0,<1.36.0)"] +billing = ["mypy-boto3-billing (>=1.35.0,<1.36.0)"] +billingconductor = ["mypy-boto3-billingconductor (>=1.35.0,<1.36.0)"] +boto3 = ["boto3 (==1.35.85)"] +braket = ["mypy-boto3-braket (>=1.35.0,<1.36.0)"] +budgets = ["mypy-boto3-budgets (>=1.35.0,<1.36.0)"] +ce = ["mypy-boto3-ce (>=1.35.0,<1.36.0)"] +chatbot = ["mypy-boto3-chatbot (>=1.35.0,<1.36.0)"] +chime = ["mypy-boto3-chime (>=1.35.0,<1.36.0)"] +chime-sdk-identity = ["mypy-boto3-chime-sdk-identity (>=1.35.0,<1.36.0)"] +chime-sdk-media-pipelines = ["mypy-boto3-chime-sdk-media-pipelines (>=1.35.0,<1.36.0)"] +chime-sdk-meetings = ["mypy-boto3-chime-sdk-meetings (>=1.35.0,<1.36.0)"] +chime-sdk-messaging = ["mypy-boto3-chime-sdk-messaging (>=1.35.0,<1.36.0)"] +chime-sdk-voice = ["mypy-boto3-chime-sdk-voice (>=1.35.0,<1.36.0)"] +cleanrooms = ["mypy-boto3-cleanrooms (>=1.35.0,<1.36.0)"] +cleanroomsml = ["mypy-boto3-cleanroomsml (>=1.35.0,<1.36.0)"] +cloud9 = ["mypy-boto3-cloud9 (>=1.35.0,<1.36.0)"] +cloudcontrol = ["mypy-boto3-cloudcontrol (>=1.35.0,<1.36.0)"] +clouddirectory = ["mypy-boto3-clouddirectory (>=1.35.0,<1.36.0)"] +cloudformation = ["mypy-boto3-cloudformation (>=1.35.0,<1.36.0)"] +cloudfront = ["mypy-boto3-cloudfront (>=1.35.0,<1.36.0)"] +cloudfront-keyvaluestore = ["mypy-boto3-cloudfront-keyvaluestore (>=1.35.0,<1.36.0)"] +cloudhsm = ["mypy-boto3-cloudhsm (>=1.35.0,<1.36.0)"] +cloudhsmv2 = ["mypy-boto3-cloudhsmv2 (>=1.35.0,<1.36.0)"] +cloudsearch = ["mypy-boto3-cloudsearch (>=1.35.0,<1.36.0)"] +cloudsearchdomain = ["mypy-boto3-cloudsearchdomain (>=1.35.0,<1.36.0)"] +cloudtrail = ["mypy-boto3-cloudtrail (>=1.35.0,<1.36.0)"] +cloudtrail-data = ["mypy-boto3-cloudtrail-data (>=1.35.0,<1.36.0)"] +cloudwatch = ["mypy-boto3-cloudwatch (>=1.35.0,<1.36.0)"] +codeartifact = ["mypy-boto3-codeartifact (>=1.35.0,<1.36.0)"] +codebuild = ["mypy-boto3-codebuild (>=1.35.0,<1.36.0)"] +codecatalyst = ["mypy-boto3-codecatalyst (>=1.35.0,<1.36.0)"] +codecommit = ["mypy-boto3-codecommit (>=1.35.0,<1.36.0)"] +codeconnections = ["mypy-boto3-codeconnections (>=1.35.0,<1.36.0)"] +codedeploy = ["mypy-boto3-codedeploy (>=1.35.0,<1.36.0)"] +codeguru-reviewer = ["mypy-boto3-codeguru-reviewer (>=1.35.0,<1.36.0)"] +codeguru-security = ["mypy-boto3-codeguru-security (>=1.35.0,<1.36.0)"] +codeguruprofiler = ["mypy-boto3-codeguruprofiler (>=1.35.0,<1.36.0)"] +codepipeline = ["mypy-boto3-codepipeline (>=1.35.0,<1.36.0)"] +codestar-connections = ["mypy-boto3-codestar-connections (>=1.35.0,<1.36.0)"] +codestar-notifications = ["mypy-boto3-codestar-notifications (>=1.35.0,<1.36.0)"] +cognito-identity = ["mypy-boto3-cognito-identity (>=1.35.0,<1.36.0)"] +cognito-idp = ["mypy-boto3-cognito-idp (>=1.35.0,<1.36.0)"] +cognito-sync = ["mypy-boto3-cognito-sync (>=1.35.0,<1.36.0)"] +comprehend = ["mypy-boto3-comprehend (>=1.35.0,<1.36.0)"] +comprehendmedical = ["mypy-boto3-comprehendmedical (>=1.35.0,<1.36.0)"] +compute-optimizer = ["mypy-boto3-compute-optimizer (>=1.35.0,<1.36.0)"] +config = ["mypy-boto3-config (>=1.35.0,<1.36.0)"] +connect = ["mypy-boto3-connect (>=1.35.0,<1.36.0)"] +connect-contact-lens = ["mypy-boto3-connect-contact-lens (>=1.35.0,<1.36.0)"] +connectcampaigns = ["mypy-boto3-connectcampaigns (>=1.35.0,<1.36.0)"] +connectcampaignsv2 = ["mypy-boto3-connectcampaignsv2 (>=1.35.0,<1.36.0)"] +connectcases = ["mypy-boto3-connectcases (>=1.35.0,<1.36.0)"] +connectparticipant = ["mypy-boto3-connectparticipant (>=1.35.0,<1.36.0)"] +controlcatalog = ["mypy-boto3-controlcatalog (>=1.35.0,<1.36.0)"] +controltower = ["mypy-boto3-controltower (>=1.35.0,<1.36.0)"] +cost-optimization-hub = ["mypy-boto3-cost-optimization-hub (>=1.35.0,<1.36.0)"] +cur = ["mypy-boto3-cur (>=1.35.0,<1.36.0)"] +customer-profiles = ["mypy-boto3-customer-profiles (>=1.35.0,<1.36.0)"] +databrew = ["mypy-boto3-databrew (>=1.35.0,<1.36.0)"] +dataexchange = ["mypy-boto3-dataexchange (>=1.35.0,<1.36.0)"] +datapipeline = ["mypy-boto3-datapipeline (>=1.35.0,<1.36.0)"] +datasync = ["mypy-boto3-datasync (>=1.35.0,<1.36.0)"] +datazone = ["mypy-boto3-datazone (>=1.35.0,<1.36.0)"] +dax = ["mypy-boto3-dax (>=1.35.0,<1.36.0)"] +deadline = ["mypy-boto3-deadline (>=1.35.0,<1.36.0)"] +detective = ["mypy-boto3-detective (>=1.35.0,<1.36.0)"] +devicefarm = ["mypy-boto3-devicefarm (>=1.35.0,<1.36.0)"] +devops-guru = ["mypy-boto3-devops-guru (>=1.35.0,<1.36.0)"] +directconnect = ["mypy-boto3-directconnect (>=1.35.0,<1.36.0)"] +discovery = ["mypy-boto3-discovery (>=1.35.0,<1.36.0)"] +dlm = ["mypy-boto3-dlm (>=1.35.0,<1.36.0)"] +dms = ["mypy-boto3-dms (>=1.35.0,<1.36.0)"] +docdb = ["mypy-boto3-docdb (>=1.35.0,<1.36.0)"] +docdb-elastic = ["mypy-boto3-docdb-elastic (>=1.35.0,<1.36.0)"] +drs = ["mypy-boto3-drs (>=1.35.0,<1.36.0)"] +ds = ["mypy-boto3-ds (>=1.35.0,<1.36.0)"] +ds-data = ["mypy-boto3-ds-data (>=1.35.0,<1.36.0)"] +dsql = ["mypy-boto3-dsql (>=1.35.0,<1.36.0)"] +dynamodb = ["mypy-boto3-dynamodb (>=1.35.0,<1.36.0)"] +dynamodbstreams = ["mypy-boto3-dynamodbstreams (>=1.35.0,<1.36.0)"] +ebs = ["mypy-boto3-ebs (>=1.35.0,<1.36.0)"] +ec2 = ["mypy-boto3-ec2 (>=1.35.0,<1.36.0)"] +ec2-instance-connect = ["mypy-boto3-ec2-instance-connect (>=1.35.0,<1.36.0)"] +ecr = ["mypy-boto3-ecr (>=1.35.0,<1.36.0)"] +ecr-public = ["mypy-boto3-ecr-public (>=1.35.0,<1.36.0)"] +ecs = ["mypy-boto3-ecs (>=1.35.0,<1.36.0)"] +efs = ["mypy-boto3-efs (>=1.35.0,<1.36.0)"] +eks = ["mypy-boto3-eks (>=1.35.0,<1.36.0)"] +eks-auth = ["mypy-boto3-eks-auth (>=1.35.0,<1.36.0)"] +elastic-inference = ["mypy-boto3-elastic-inference (>=1.35.0,<1.36.0)"] +elasticache = ["mypy-boto3-elasticache (>=1.35.0,<1.36.0)"] +elasticbeanstalk = ["mypy-boto3-elasticbeanstalk (>=1.35.0,<1.36.0)"] +elastictranscoder = ["mypy-boto3-elastictranscoder (>=1.35.0,<1.36.0)"] +elb = ["mypy-boto3-elb (>=1.35.0,<1.36.0)"] +elbv2 = ["mypy-boto3-elbv2 (>=1.35.0,<1.36.0)"] +emr = ["mypy-boto3-emr (>=1.35.0,<1.36.0)"] +emr-containers = ["mypy-boto3-emr-containers (>=1.35.0,<1.36.0)"] +emr-serverless = ["mypy-boto3-emr-serverless (>=1.35.0,<1.36.0)"] +entityresolution = ["mypy-boto3-entityresolution (>=1.35.0,<1.36.0)"] +es = ["mypy-boto3-es (>=1.35.0,<1.36.0)"] +essential = ["mypy-boto3-cloudformation (>=1.35.0,<1.36.0)", "mypy-boto3-dynamodb (>=1.35.0,<1.36.0)", "mypy-boto3-ec2 (>=1.35.0,<1.36.0)", "mypy-boto3-lambda (>=1.35.0,<1.36.0)", "mypy-boto3-rds (>=1.35.0,<1.36.0)", "mypy-boto3-s3 (>=1.35.0,<1.36.0)", "mypy-boto3-sqs (>=1.35.0,<1.36.0)"] +events = ["mypy-boto3-events (>=1.35.0,<1.36.0)"] +evidently = ["mypy-boto3-evidently (>=1.35.0,<1.36.0)"] +finspace = ["mypy-boto3-finspace (>=1.35.0,<1.36.0)"] +finspace-data = ["mypy-boto3-finspace-data (>=1.35.0,<1.36.0)"] +firehose = ["mypy-boto3-firehose (>=1.35.0,<1.36.0)"] +fis = ["mypy-boto3-fis (>=1.35.0,<1.36.0)"] +fms = ["mypy-boto3-fms (>=1.35.0,<1.36.0)"] +forecast = ["mypy-boto3-forecast (>=1.35.0,<1.36.0)"] +forecastquery = ["mypy-boto3-forecastquery (>=1.35.0,<1.36.0)"] +frauddetector = ["mypy-boto3-frauddetector (>=1.35.0,<1.36.0)"] +freetier = ["mypy-boto3-freetier (>=1.35.0,<1.36.0)"] +fsx = ["mypy-boto3-fsx (>=1.35.0,<1.36.0)"] +full = ["boto3-stubs-full (>=1.35.0,<1.36.0)"] +gamelift = ["mypy-boto3-gamelift (>=1.35.0,<1.36.0)"] +geo-maps = ["mypy-boto3-geo-maps (>=1.35.0,<1.36.0)"] +geo-places = ["mypy-boto3-geo-places (>=1.35.0,<1.36.0)"] +geo-routes = ["mypy-boto3-geo-routes (>=1.35.0,<1.36.0)"] +glacier = ["mypy-boto3-glacier (>=1.35.0,<1.36.0)"] +globalaccelerator = ["mypy-boto3-globalaccelerator (>=1.35.0,<1.36.0)"] +glue = ["mypy-boto3-glue (>=1.35.0,<1.36.0)"] +grafana = ["mypy-boto3-grafana (>=1.35.0,<1.36.0)"] +greengrass = ["mypy-boto3-greengrass (>=1.35.0,<1.36.0)"] +greengrassv2 = ["mypy-boto3-greengrassv2 (>=1.35.0,<1.36.0)"] +groundstation = ["mypy-boto3-groundstation (>=1.35.0,<1.36.0)"] +guardduty = ["mypy-boto3-guardduty (>=1.35.0,<1.36.0)"] +health = ["mypy-boto3-health (>=1.35.0,<1.36.0)"] +healthlake = ["mypy-boto3-healthlake (>=1.35.0,<1.36.0)"] +iam = ["mypy-boto3-iam (>=1.35.0,<1.36.0)"] +identitystore = ["mypy-boto3-identitystore (>=1.35.0,<1.36.0)"] +imagebuilder = ["mypy-boto3-imagebuilder (>=1.35.0,<1.36.0)"] +importexport = ["mypy-boto3-importexport (>=1.35.0,<1.36.0)"] +inspector = ["mypy-boto3-inspector (>=1.35.0,<1.36.0)"] +inspector-scan = ["mypy-boto3-inspector-scan (>=1.35.0,<1.36.0)"] +inspector2 = ["mypy-boto3-inspector2 (>=1.35.0,<1.36.0)"] +internetmonitor = ["mypy-boto3-internetmonitor (>=1.35.0,<1.36.0)"] +invoicing = ["mypy-boto3-invoicing (>=1.35.0,<1.36.0)"] +iot = ["mypy-boto3-iot (>=1.35.0,<1.36.0)"] +iot-data = ["mypy-boto3-iot-data (>=1.35.0,<1.36.0)"] +iot-jobs-data = ["mypy-boto3-iot-jobs-data (>=1.35.0,<1.36.0)"] +iot1click-devices = ["mypy-boto3-iot1click-devices (>=1.35.0,<1.36.0)"] +iot1click-projects = ["mypy-boto3-iot1click-projects (>=1.35.0,<1.36.0)"] +iotanalytics = ["mypy-boto3-iotanalytics (>=1.35.0,<1.36.0)"] +iotdeviceadvisor = ["mypy-boto3-iotdeviceadvisor (>=1.35.0,<1.36.0)"] +iotevents = ["mypy-boto3-iotevents (>=1.35.0,<1.36.0)"] +iotevents-data = ["mypy-boto3-iotevents-data (>=1.35.0,<1.36.0)"] +iotfleethub = ["mypy-boto3-iotfleethub (>=1.35.0,<1.36.0)"] +iotfleetwise = ["mypy-boto3-iotfleetwise (>=1.35.0,<1.36.0)"] +iotsecuretunneling = ["mypy-boto3-iotsecuretunneling (>=1.35.0,<1.36.0)"] +iotsitewise = ["mypy-boto3-iotsitewise (>=1.35.0,<1.36.0)"] +iotthingsgraph = ["mypy-boto3-iotthingsgraph (>=1.35.0,<1.36.0)"] +iottwinmaker = ["mypy-boto3-iottwinmaker (>=1.35.0,<1.36.0)"] +iotwireless = ["mypy-boto3-iotwireless (>=1.35.0,<1.36.0)"] +ivs = ["mypy-boto3-ivs (>=1.35.0,<1.36.0)"] +ivs-realtime = ["mypy-boto3-ivs-realtime (>=1.35.0,<1.36.0)"] +ivschat = ["mypy-boto3-ivschat (>=1.35.0,<1.36.0)"] +kafka = ["mypy-boto3-kafka (>=1.35.0,<1.36.0)"] +kafkaconnect = ["mypy-boto3-kafkaconnect (>=1.35.0,<1.36.0)"] +kendra = ["mypy-boto3-kendra (>=1.35.0,<1.36.0)"] +kendra-ranking = ["mypy-boto3-kendra-ranking (>=1.35.0,<1.36.0)"] +keyspaces = ["mypy-boto3-keyspaces (>=1.35.0,<1.36.0)"] +kinesis = ["mypy-boto3-kinesis (>=1.35.0,<1.36.0)"] +kinesis-video-archived-media = ["mypy-boto3-kinesis-video-archived-media (>=1.35.0,<1.36.0)"] +kinesis-video-media = ["mypy-boto3-kinesis-video-media (>=1.35.0,<1.36.0)"] +kinesis-video-signaling = ["mypy-boto3-kinesis-video-signaling (>=1.35.0,<1.36.0)"] +kinesis-video-webrtc-storage = ["mypy-boto3-kinesis-video-webrtc-storage (>=1.35.0,<1.36.0)"] +kinesisanalytics = ["mypy-boto3-kinesisanalytics (>=1.35.0,<1.36.0)"] +kinesisanalyticsv2 = ["mypy-boto3-kinesisanalyticsv2 (>=1.35.0,<1.36.0)"] +kinesisvideo = ["mypy-boto3-kinesisvideo (>=1.35.0,<1.36.0)"] +kms = ["mypy-boto3-kms (>=1.35.0,<1.36.0)"] +lakeformation = ["mypy-boto3-lakeformation (>=1.35.0,<1.36.0)"] +lambda = ["mypy-boto3-lambda (>=1.35.0,<1.36.0)"] +launch-wizard = ["mypy-boto3-launch-wizard (>=1.35.0,<1.36.0)"] +lex-models = ["mypy-boto3-lex-models (>=1.35.0,<1.36.0)"] +lex-runtime = ["mypy-boto3-lex-runtime (>=1.35.0,<1.36.0)"] +lexv2-models = ["mypy-boto3-lexv2-models (>=1.35.0,<1.36.0)"] +lexv2-runtime = ["mypy-boto3-lexv2-runtime (>=1.35.0,<1.36.0)"] +license-manager = ["mypy-boto3-license-manager (>=1.35.0,<1.36.0)"] +license-manager-linux-subscriptions = ["mypy-boto3-license-manager-linux-subscriptions (>=1.35.0,<1.36.0)"] +license-manager-user-subscriptions = ["mypy-boto3-license-manager-user-subscriptions (>=1.35.0,<1.36.0)"] +lightsail = ["mypy-boto3-lightsail (>=1.35.0,<1.36.0)"] +location = ["mypy-boto3-location (>=1.35.0,<1.36.0)"] +logs = ["mypy-boto3-logs (>=1.35.0,<1.36.0)"] +lookoutequipment = ["mypy-boto3-lookoutequipment (>=1.35.0,<1.36.0)"] +lookoutmetrics = ["mypy-boto3-lookoutmetrics (>=1.35.0,<1.36.0)"] +lookoutvision = ["mypy-boto3-lookoutvision (>=1.35.0,<1.36.0)"] +m2 = ["mypy-boto3-m2 (>=1.35.0,<1.36.0)"] +machinelearning = ["mypy-boto3-machinelearning (>=1.35.0,<1.36.0)"] +macie2 = ["mypy-boto3-macie2 (>=1.35.0,<1.36.0)"] +mailmanager = ["mypy-boto3-mailmanager (>=1.35.0,<1.36.0)"] +managedblockchain = ["mypy-boto3-managedblockchain (>=1.35.0,<1.36.0)"] +managedblockchain-query = ["mypy-boto3-managedblockchain-query (>=1.35.0,<1.36.0)"] +marketplace-agreement = ["mypy-boto3-marketplace-agreement (>=1.35.0,<1.36.0)"] +marketplace-catalog = ["mypy-boto3-marketplace-catalog (>=1.35.0,<1.36.0)"] +marketplace-deployment = ["mypy-boto3-marketplace-deployment (>=1.35.0,<1.36.0)"] +marketplace-entitlement = ["mypy-boto3-marketplace-entitlement (>=1.35.0,<1.36.0)"] +marketplace-reporting = ["mypy-boto3-marketplace-reporting (>=1.35.0,<1.36.0)"] +marketplacecommerceanalytics = ["mypy-boto3-marketplacecommerceanalytics (>=1.35.0,<1.36.0)"] +mediaconnect = ["mypy-boto3-mediaconnect (>=1.35.0,<1.36.0)"] +mediaconvert = ["mypy-boto3-mediaconvert (>=1.35.0,<1.36.0)"] +medialive = ["mypy-boto3-medialive (>=1.35.0,<1.36.0)"] +mediapackage = ["mypy-boto3-mediapackage (>=1.35.0,<1.36.0)"] +mediapackage-vod = ["mypy-boto3-mediapackage-vod (>=1.35.0,<1.36.0)"] +mediapackagev2 = ["mypy-boto3-mediapackagev2 (>=1.35.0,<1.36.0)"] +mediastore = ["mypy-boto3-mediastore (>=1.35.0,<1.36.0)"] +mediastore-data = ["mypy-boto3-mediastore-data (>=1.35.0,<1.36.0)"] +mediatailor = ["mypy-boto3-mediatailor (>=1.35.0,<1.36.0)"] +medical-imaging = ["mypy-boto3-medical-imaging (>=1.35.0,<1.36.0)"] +memorydb = ["mypy-boto3-memorydb (>=1.35.0,<1.36.0)"] +meteringmarketplace = ["mypy-boto3-meteringmarketplace (>=1.35.0,<1.36.0)"] +mgh = ["mypy-boto3-mgh (>=1.35.0,<1.36.0)"] +mgn = ["mypy-boto3-mgn (>=1.35.0,<1.36.0)"] +migration-hub-refactor-spaces = ["mypy-boto3-migration-hub-refactor-spaces (>=1.35.0,<1.36.0)"] +migrationhub-config = ["mypy-boto3-migrationhub-config (>=1.35.0,<1.36.0)"] +migrationhuborchestrator = ["mypy-boto3-migrationhuborchestrator (>=1.35.0,<1.36.0)"] +migrationhubstrategy = ["mypy-boto3-migrationhubstrategy (>=1.35.0,<1.36.0)"] +mq = ["mypy-boto3-mq (>=1.35.0,<1.36.0)"] +mturk = ["mypy-boto3-mturk (>=1.35.0,<1.36.0)"] +mwaa = ["mypy-boto3-mwaa (>=1.35.0,<1.36.0)"] +neptune = ["mypy-boto3-neptune (>=1.35.0,<1.36.0)"] +neptune-graph = ["mypy-boto3-neptune-graph (>=1.35.0,<1.36.0)"] +neptunedata = ["mypy-boto3-neptunedata (>=1.35.0,<1.36.0)"] +network-firewall = ["mypy-boto3-network-firewall (>=1.35.0,<1.36.0)"] +networkflowmonitor = ["mypy-boto3-networkflowmonitor (>=1.35.0,<1.36.0)"] +networkmanager = ["mypy-boto3-networkmanager (>=1.35.0,<1.36.0)"] +networkmonitor = ["mypy-boto3-networkmonitor (>=1.35.0,<1.36.0)"] +notifications = ["mypy-boto3-notifications (>=1.35.0,<1.36.0)"] +notificationscontacts = ["mypy-boto3-notificationscontacts (>=1.35.0,<1.36.0)"] +oam = ["mypy-boto3-oam (>=1.35.0,<1.36.0)"] +observabilityadmin = ["mypy-boto3-observabilityadmin (>=1.35.0,<1.36.0)"] +omics = ["mypy-boto3-omics (>=1.35.0,<1.36.0)"] +opensearch = ["mypy-boto3-opensearch (>=1.35.0,<1.36.0)"] +opensearchserverless = ["mypy-boto3-opensearchserverless (>=1.35.0,<1.36.0)"] +opsworks = ["mypy-boto3-opsworks (>=1.35.0,<1.36.0)"] +opsworkscm = ["mypy-boto3-opsworkscm (>=1.35.0,<1.36.0)"] +organizations = ["mypy-boto3-organizations (>=1.35.0,<1.36.0)"] +osis = ["mypy-boto3-osis (>=1.35.0,<1.36.0)"] +outposts = ["mypy-boto3-outposts (>=1.35.0,<1.36.0)"] +panorama = ["mypy-boto3-panorama (>=1.35.0,<1.36.0)"] +partnercentral-selling = ["mypy-boto3-partnercentral-selling (>=1.35.0,<1.36.0)"] +payment-cryptography = ["mypy-boto3-payment-cryptography (>=1.35.0,<1.36.0)"] +payment-cryptography-data = ["mypy-boto3-payment-cryptography-data (>=1.35.0,<1.36.0)"] +pca-connector-ad = ["mypy-boto3-pca-connector-ad (>=1.35.0,<1.36.0)"] +pca-connector-scep = ["mypy-boto3-pca-connector-scep (>=1.35.0,<1.36.0)"] +pcs = ["mypy-boto3-pcs (>=1.35.0,<1.36.0)"] +personalize = ["mypy-boto3-personalize (>=1.35.0,<1.36.0)"] +personalize-events = ["mypy-boto3-personalize-events (>=1.35.0,<1.36.0)"] +personalize-runtime = ["mypy-boto3-personalize-runtime (>=1.35.0,<1.36.0)"] +pi = ["mypy-boto3-pi (>=1.35.0,<1.36.0)"] +pinpoint = ["mypy-boto3-pinpoint (>=1.35.0,<1.36.0)"] +pinpoint-email = ["mypy-boto3-pinpoint-email (>=1.35.0,<1.36.0)"] +pinpoint-sms-voice = ["mypy-boto3-pinpoint-sms-voice (>=1.35.0,<1.36.0)"] +pinpoint-sms-voice-v2 = ["mypy-boto3-pinpoint-sms-voice-v2 (>=1.35.0,<1.36.0)"] +pipes = ["mypy-boto3-pipes (>=1.35.0,<1.36.0)"] +polly = ["mypy-boto3-polly (>=1.35.0,<1.36.0)"] +pricing = ["mypy-boto3-pricing (>=1.35.0,<1.36.0)"] +privatenetworks = ["mypy-boto3-privatenetworks (>=1.35.0,<1.36.0)"] +proton = ["mypy-boto3-proton (>=1.35.0,<1.36.0)"] +qapps = ["mypy-boto3-qapps (>=1.35.0,<1.36.0)"] +qbusiness = ["mypy-boto3-qbusiness (>=1.35.0,<1.36.0)"] +qconnect = ["mypy-boto3-qconnect (>=1.35.0,<1.36.0)"] +qldb = ["mypy-boto3-qldb (>=1.35.0,<1.36.0)"] +qldb-session = ["mypy-boto3-qldb-session (>=1.35.0,<1.36.0)"] +quicksight = ["mypy-boto3-quicksight (>=1.35.0,<1.36.0)"] +ram = ["mypy-boto3-ram (>=1.35.0,<1.36.0)"] +rbin = ["mypy-boto3-rbin (>=1.35.0,<1.36.0)"] +rds = ["mypy-boto3-rds (>=1.35.0,<1.36.0)"] +rds-data = ["mypy-boto3-rds-data (>=1.35.0,<1.36.0)"] +redshift = ["mypy-boto3-redshift (>=1.35.0,<1.36.0)"] +redshift-data = ["mypy-boto3-redshift-data (>=1.35.0,<1.36.0)"] +redshift-serverless = ["mypy-boto3-redshift-serverless (>=1.35.0,<1.36.0)"] +rekognition = ["mypy-boto3-rekognition (>=1.35.0,<1.36.0)"] +repostspace = ["mypy-boto3-repostspace (>=1.35.0,<1.36.0)"] +resiliencehub = ["mypy-boto3-resiliencehub (>=1.35.0,<1.36.0)"] +resource-explorer-2 = ["mypy-boto3-resource-explorer-2 (>=1.35.0,<1.36.0)"] +resource-groups = ["mypy-boto3-resource-groups (>=1.35.0,<1.36.0)"] +resourcegroupstaggingapi = ["mypy-boto3-resourcegroupstaggingapi (>=1.35.0,<1.36.0)"] +robomaker = ["mypy-boto3-robomaker (>=1.35.0,<1.36.0)"] +rolesanywhere = ["mypy-boto3-rolesanywhere (>=1.35.0,<1.36.0)"] +route53 = ["mypy-boto3-route53 (>=1.35.0,<1.36.0)"] +route53-recovery-cluster = ["mypy-boto3-route53-recovery-cluster (>=1.35.0,<1.36.0)"] +route53-recovery-control-config = ["mypy-boto3-route53-recovery-control-config (>=1.35.0,<1.36.0)"] +route53-recovery-readiness = ["mypy-boto3-route53-recovery-readiness (>=1.35.0,<1.36.0)"] +route53domains = ["mypy-boto3-route53domains (>=1.35.0,<1.36.0)"] +route53profiles = ["mypy-boto3-route53profiles (>=1.35.0,<1.36.0)"] +route53resolver = ["mypy-boto3-route53resolver (>=1.35.0,<1.36.0)"] +rum = ["mypy-boto3-rum (>=1.35.0,<1.36.0)"] +s3 = ["mypy-boto3-s3 (>=1.35.0,<1.36.0)"] +s3control = ["mypy-boto3-s3control (>=1.35.0,<1.36.0)"] +s3outposts = ["mypy-boto3-s3outposts (>=1.35.0,<1.36.0)"] +s3tables = ["mypy-boto3-s3tables (>=1.35.0,<1.36.0)"] +sagemaker = ["mypy-boto3-sagemaker (>=1.35.0,<1.36.0)"] +sagemaker-a2i-runtime = ["mypy-boto3-sagemaker-a2i-runtime (>=1.35.0,<1.36.0)"] +sagemaker-edge = ["mypy-boto3-sagemaker-edge (>=1.35.0,<1.36.0)"] +sagemaker-featurestore-runtime = ["mypy-boto3-sagemaker-featurestore-runtime (>=1.35.0,<1.36.0)"] +sagemaker-geospatial = ["mypy-boto3-sagemaker-geospatial (>=1.35.0,<1.36.0)"] +sagemaker-metrics = ["mypy-boto3-sagemaker-metrics (>=1.35.0,<1.36.0)"] +sagemaker-runtime = ["mypy-boto3-sagemaker-runtime (>=1.35.0,<1.36.0)"] +savingsplans = ["mypy-boto3-savingsplans (>=1.35.0,<1.36.0)"] +scheduler = ["mypy-boto3-scheduler (>=1.35.0,<1.36.0)"] +schemas = ["mypy-boto3-schemas (>=1.35.0,<1.36.0)"] +sdb = ["mypy-boto3-sdb (>=1.35.0,<1.36.0)"] +secretsmanager = ["mypy-boto3-secretsmanager (>=1.35.0,<1.36.0)"] +security-ir = ["mypy-boto3-security-ir (>=1.35.0,<1.36.0)"] +securityhub = ["mypy-boto3-securityhub (>=1.35.0,<1.36.0)"] +securitylake = ["mypy-boto3-securitylake (>=1.35.0,<1.36.0)"] +serverlessrepo = ["mypy-boto3-serverlessrepo (>=1.35.0,<1.36.0)"] +service-quotas = ["mypy-boto3-service-quotas (>=1.35.0,<1.36.0)"] +servicecatalog = ["mypy-boto3-servicecatalog (>=1.35.0,<1.36.0)"] +servicecatalog-appregistry = ["mypy-boto3-servicecatalog-appregistry (>=1.35.0,<1.36.0)"] +servicediscovery = ["mypy-boto3-servicediscovery (>=1.35.0,<1.36.0)"] +ses = ["mypy-boto3-ses (>=1.35.0,<1.36.0)"] +sesv2 = ["mypy-boto3-sesv2 (>=1.35.0,<1.36.0)"] +shield = ["mypy-boto3-shield (>=1.35.0,<1.36.0)"] +signer = ["mypy-boto3-signer (>=1.35.0,<1.36.0)"] +simspaceweaver = ["mypy-boto3-simspaceweaver (>=1.35.0,<1.36.0)"] +sms = ["mypy-boto3-sms (>=1.35.0,<1.36.0)"] +sms-voice = ["mypy-boto3-sms-voice (>=1.35.0,<1.36.0)"] +snow-device-management = ["mypy-boto3-snow-device-management (>=1.35.0,<1.36.0)"] +snowball = ["mypy-boto3-snowball (>=1.35.0,<1.36.0)"] +sns = ["mypy-boto3-sns (>=1.35.0,<1.36.0)"] +socialmessaging = ["mypy-boto3-socialmessaging (>=1.35.0,<1.36.0)"] +sqs = ["mypy-boto3-sqs (>=1.35.0,<1.36.0)"] +ssm = ["mypy-boto3-ssm (>=1.35.0,<1.36.0)"] +ssm-contacts = ["mypy-boto3-ssm-contacts (>=1.35.0,<1.36.0)"] +ssm-incidents = ["mypy-boto3-ssm-incidents (>=1.35.0,<1.36.0)"] +ssm-quicksetup = ["mypy-boto3-ssm-quicksetup (>=1.35.0,<1.36.0)"] +ssm-sap = ["mypy-boto3-ssm-sap (>=1.35.0,<1.36.0)"] +sso = ["mypy-boto3-sso (>=1.35.0,<1.36.0)"] +sso-admin = ["mypy-boto3-sso-admin (>=1.35.0,<1.36.0)"] +sso-oidc = ["mypy-boto3-sso-oidc (>=1.35.0,<1.36.0)"] +stepfunctions = ["mypy-boto3-stepfunctions (>=1.35.0,<1.36.0)"] +storagegateway = ["mypy-boto3-storagegateway (>=1.35.0,<1.36.0)"] +sts = ["mypy-boto3-sts (>=1.35.0,<1.36.0)"] +supplychain = ["mypy-boto3-supplychain (>=1.35.0,<1.36.0)"] +support = ["mypy-boto3-support (>=1.35.0,<1.36.0)"] +support-app = ["mypy-boto3-support-app (>=1.35.0,<1.36.0)"] +swf = ["mypy-boto3-swf (>=1.35.0,<1.36.0)"] +synthetics = ["mypy-boto3-synthetics (>=1.35.0,<1.36.0)"] +taxsettings = ["mypy-boto3-taxsettings (>=1.35.0,<1.36.0)"] +textract = ["mypy-boto3-textract (>=1.35.0,<1.36.0)"] +timestream-influxdb = ["mypy-boto3-timestream-influxdb (>=1.35.0,<1.36.0)"] +timestream-query = ["mypy-boto3-timestream-query (>=1.35.0,<1.36.0)"] +timestream-write = ["mypy-boto3-timestream-write (>=1.35.0,<1.36.0)"] +tnb = ["mypy-boto3-tnb (>=1.35.0,<1.36.0)"] +transcribe = ["mypy-boto3-transcribe (>=1.35.0,<1.36.0)"] +transfer = ["mypy-boto3-transfer (>=1.35.0,<1.36.0)"] +translate = ["mypy-boto3-translate (>=1.35.0,<1.36.0)"] +trustedadvisor = ["mypy-boto3-trustedadvisor (>=1.35.0,<1.36.0)"] +verifiedpermissions = ["mypy-boto3-verifiedpermissions (>=1.35.0,<1.36.0)"] +voice-id = ["mypy-boto3-voice-id (>=1.35.0,<1.36.0)"] +vpc-lattice = ["mypy-boto3-vpc-lattice (>=1.35.0,<1.36.0)"] +waf = ["mypy-boto3-waf (>=1.35.0,<1.36.0)"] +waf-regional = ["mypy-boto3-waf-regional (>=1.35.0,<1.36.0)"] +wafv2 = ["mypy-boto3-wafv2 (>=1.35.0,<1.36.0)"] +wellarchitected = ["mypy-boto3-wellarchitected (>=1.35.0,<1.36.0)"] +wisdom = ["mypy-boto3-wisdom (>=1.35.0,<1.36.0)"] +workdocs = ["mypy-boto3-workdocs (>=1.35.0,<1.36.0)"] +workmail = ["mypy-boto3-workmail (>=1.35.0,<1.36.0)"] +workmailmessageflow = ["mypy-boto3-workmailmessageflow (>=1.35.0,<1.36.0)"] +workspaces = ["mypy-boto3-workspaces (>=1.35.0,<1.36.0)"] +workspaces-thin-client = ["mypy-boto3-workspaces-thin-client (>=1.35.0,<1.36.0)"] +workspaces-web = ["mypy-boto3-workspaces-web (>=1.35.0,<1.36.0)"] +xray = ["mypy-boto3-xray (>=1.35.0,<1.36.0)"] + +[[package]] +name = "botocore" +version = "1.34.162" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +files = [ + {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, + {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, +] + +[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.21.2)"] + +[[package]] +name = "botocore-stubs" +version = "1.35.84.post1" +description = "Type annotations and code completion for botocore" +optional = false +python-versions = ">=3.8" +files = [ + {file = "botocore_stubs-1.35.84.post1-py3-none-any.whl", hash = "sha256:bb0d6fb74161944ab61a66ae2d891b780c163e89336f24462d0e8aed5b9ff4b9"}, + {file = "botocore_stubs-1.35.84.post1.tar.gz", hash = "sha256:ead1b807a3baa77962bc89ab701616425374ba5d31219fb64c2ca090f33090b3"}, +] + +[package.dependencies] +types-awscrt = "*" + +[package.extras] +botocore = ["botocore"] + +[[package]] +name = "build" +version = "1.2.2.post1" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, + {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +importlib-metadata = {version = ">=4.6", markers = "python_full_version < \"3.10.2\""} +packaging = ">=19.1" +pyproject_hooks = "*" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"] +test = ["build[uv,virtualenv]", "filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"] +typing = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.0.35)"] + +[[package]] +name = "bump-my-version" +version = "0.19.3" +description = "Version bump your Python project" +optional = false +python-versions = ">=3.8" +files = [ + {file = "bump-my-version-0.19.3.tar.gz", hash = "sha256:f1c3f686bbac2b9de0fd883ca327167b66e67189e9246b8978af48b43462fdbb"}, + {file = "bump_my_version-0.19.3-py3-none-any.whl", hash = "sha256:778da1333ce5ee7dee3dcd119a7fed04c4294ade20cc04c087c10465bf534ee6"}, +] + +[package.dependencies] +click = "*" +pydantic = ">=2.0.0" +pydantic-settings = "*" +questionary = "*" +rich = "*" +rich-click = "*" +tomlkit = "*" + +[package.extras] +dev = ["generate-changelog (>=0.7.6)", "git-fame (>=1.12.2)", "pip-tools", "pre-commit"] +docs = ["black", "markdown-customblocks", "mdx-truly-sane-lists", "mkdocs", "mkdocs-click", "mkdocs-drawio", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-git-committers-plugin", "mkdocs-git-revision-date-localized-plugin", "mkdocs-include-markdown-plugin", "mkdocs-literate-nav", "mkdocs-material", "mkdocstrings[python]", "python-frontmatter"] +test = ["coverage", "pre-commit", "pytest", "pytest-cov", "pytest-mock"] + +[[package]] +name = "certifi" +version = "2024.12.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.9" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, + {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, + {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, + {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, + {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, + {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, + {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, + {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, + {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, + {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, + {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, + {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, + {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, + {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, + {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, + {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, + {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, + {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, + {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, + {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, + {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, + {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, + {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, + {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, + {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, + {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, + {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, + {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, + {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, + {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, + {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, + {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, + {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "44.0.0" +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.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "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 = "cssbeautifier" +version = "1.15.1" +description = "CSS unobfuscator and beautifier." +optional = false +python-versions = "*" +files = [ + {file = "cssbeautifier-1.15.1.tar.gz", hash = "sha256:9f7064362aedd559c55eeecf6b6bed65e05f33488dcbe39044f0403c26e1c006"}, +] + +[package.dependencies] +editorconfig = ">=0.12.2" +jsbeautifier = "*" +six = ">=1.13.0" + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "django" +version = "5.1.4" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.10" +files = [ + {file = "Django-5.1.4-py3-none-any.whl", hash = "sha256:236e023f021f5ce7dee5779de7b286565fdea5f4ab86bae5338e3f7b69896cf0"}, + {file = "Django-5.1.4.tar.gz", hash = "sha256:de450c09e91879fa5a307f696e57c851955c910a438a35e6b4c895e86bedc82a"}, +] + +[package.dependencies] +asgiref = ">=3.8.1,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-environ" +version = "0.11.2" +description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, + {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, +] + +[package.extras] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] + +[[package]] +name = "django-login-required-middleware" +version = "0.9.0" +description = "Requires login to all requests through middleware." +optional = false +python-versions = "*" +files = [ + {file = "django-login-required-middleware-0.9.0.tar.gz", hash = "sha256:847ae9a69fd7a07618ed53192b3c06946af70a0caf6d0f4eb40a8f37593cd970"}, +] + +[[package]] +name = "django-ratelimit" +version = "4.1.0" +description = "Cache-based rate-limiting for Django." +optional = false +python-versions = ">=3.7" +files = [ + {file = "django-ratelimit-4.1.0.tar.gz", hash = "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b"}, + {file = "django_ratelimit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d047a31cf94d83ef1465d7543ca66c6fc16695559b5f8d814d1b51df15110b92"}, +] + +[[package]] +name = "django-silk" +version = "5.3.2" +description = "Silky smooth profiling for the Django Framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "django_silk-5.3.2-py3-none-any.whl", hash = "sha256:49f1caebfda28b1707f0cfef524e0476beb82b8c5e40f5ccff7f73a6b4f6d3ac"}, + {file = "django_silk-5.3.2.tar.gz", hash = "sha256:b0db54eebedb8d16f572321bd6daccac0bd3f547ae2618bb45d96fe8fc02229d"}, +] + +[package.dependencies] +autopep8 = "*" +Django = ">=4.2" +gprof2dot = ">=2017.09.19" +sqlparse = "*" + +[[package]] +name = "django-storages" +version = "1.14.2" +description = "Support for many storage backends in Django" +optional = false +python-versions = ">=3.7" +files = [ + {file = "django-storages-1.14.2.tar.gz", hash = "sha256:51b36af28cc5813b98d5f3dfe7459af638d84428c8df4a03990c7d74d1bea4e5"}, + {file = "django_storages-1.14.2-py3-none-any.whl", hash = "sha256:1db759346b52ada6c2efd9f23d8241ecf518813eb31db9e2589207174f58f6ad"}, +] + +[package.dependencies] +Django = ">=3.2" + +[package.extras] +azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"] +boto3 = ["boto3 (>=1.4.4)"] +dropbox = ["dropbox (>=7.2.1)"] +google = ["google-cloud-storage (>=1.27)"] +libcloud = ["apache-libcloud"] +s3 = ["boto3 (>=1.4.4)"] +sftp = ["paramiko (>=1.15)"] + +[[package]] +name = "django-stubs" +version = "4.2.7" +description = "Mypy stubs for Django" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"}, + {file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"}, +] + +[package.dependencies] +django = "*" +django-stubs-ext = ">=4.2.7" +mypy = {version = ">=1.7.0,<1.8.0", optional = true, markers = "extra == \"compatible-mypy\""} +tomli = {version = "*", markers = "python_version < \"3.11\""} +types-pytz = "*" +types-PyYAML = "*" +typing-extensions = "*" + +[package.extras] +compatible-mypy = ["mypy (>=1.7.0,<1.8.0)"] + +[[package]] +name = "django-stubs-ext" +version = "4.2.7" +description = "Monkey-patching and extensions for django-stubs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-stubs-ext-4.2.7.tar.gz", hash = "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"}, + {file = "django_stubs_ext-4.2.7-py3-none-any.whl", hash = "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c"}, +] + +[package.dependencies] +django = "*" +typing-extensions = "*" + +[[package]] +name = "djangorestframework" +version = "3.15.2" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, + {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, +] + +[package.dependencies] +django = ">=4.2" + +[[package]] +name = "djangorestframework-stubs" +version = "3.14.5" +description = "PEP-484 stubs for django-rest-framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"}, + {file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"}, +] + +[package.dependencies] +django-stubs = [ + {version = ">=4.2.7"}, + {version = "*", extras = ["compatible-mypy"], optional = true, markers = "extra == \"compatible-mypy\""}, +] +mypy = {version = ">=1.7.0,<1.8.0", optional = true, markers = "extra == \"compatible-mypy\""} +requests = ">=2.0.0" +types-PyYAML = ">=5.4.3" +types-requests = ">=0.1.12" +typing-extensions = ">=3.10.0" + +[package.extras] +compatible-mypy = ["django-stubs[compatible-mypy]", "mypy (>=1.7.0,<1.8.0)"] +coreapi = ["coreapi (>=2.0.0)"] +markdown = ["types-Markdown (>=0.1.5)"] + +[[package]] +name = "djlint" +version = "1.36.3" +description = "HTML Template Linter and Formatter" +optional = false +python-versions = ">=3.9" +files = [ + {file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"}, + {file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"}, + {file = "djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579"}, + {file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"}, + {file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"}, + {file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"}, + {file = "djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea"}, + {file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"}, + {file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"}, + {file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"}, + {file = "djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a"}, + {file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"}, + {file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"}, + {file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"}, + {file = "djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0"}, + {file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"}, + {file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"}, + {file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"}, + {file = "djlint-1.36.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b2101c2d1b079e8d545e6d9d03487fcca14d2371e44cbfdedee15b0bf4567c"}, + {file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"}, + {file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"}, + {file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"}, +] + +[package.dependencies] +click = ">=8.0.1" +colorama = ">=0.4.4" +cssbeautifier = ">=1.14.4" +jsbeautifier = ">=1.14.4" +json5 = ">=0.9.11" +pathspec = ">=0.12" +pyyaml = ">=6" +regex = ">=2023" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +tqdm = ">=4.62.2" + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "editorconfig" +version = "0.17.0" +description = "EditorConfig File Locator and Interpreter for Python" +optional = false +python-versions = "*" +files = [ + {file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"}, + {file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"}, +] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.43" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"}, + {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"] +test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] + +[[package]] +name = "gprof2dot" +version = "2024.6.6" +description = "Generate a dot graph from the output of several profilers." +optional = false +python-versions = ">=3.8" +files = [ + {file = "gprof2dot-2024.6.6-py2.py3-none-any.whl", hash = "sha256:45b14ad7ce64e299c8f526881007b9eb2c6b75505d5613e96e66ee4d5ab33696"}, + {file = "gprof2dot-2024.6.6.tar.gz", hash = "sha256:fa1420c60025a9eb7734f65225b4da02a10fc6dd741b37fa129bc6b41951e5ab"}, +] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, + {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +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 = "jsbeautifier" +version = "1.15.1" +description = "JavaScript unobfuscator and beautifier." +optional = false +python-versions = "*" +files = [ + {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, +] + +[package.dependencies] +editorconfig = ">=0.12.2" +six = ">=1.13.0" + +[[package]] +name = "json5" +version = "0.10.0" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, + {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, +] + +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] + +[[package]] +name = "keyring" +version = "25.5.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, + {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] + +[[package]] +name = "mariadb" +version = "1.1.8" +description = "Python MariaDB extension" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mariadb-1.1.8-cp310-cp310-win32.whl", hash = "sha256:afe819f5b68ee24e73a7bdb3bb72a7367fcd03659493988363b74733c0466492"}, + {file = "mariadb-1.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:525ed4653bd6aef910fb4620e3d6358461e96d002d9db269c2abf668e9e8460b"}, + {file = "mariadb-1.1.8-cp311-cp311-win32.whl", hash = "sha256:bdfe41d29c87a60ad80d7b8b56af6cd40f3d07abc4d092f30f8b24184bd7d3fb"}, + {file = "mariadb-1.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:3224eaaee09401558a022d00441663019b9090ffa558342389efe61bfee791e1"}, + {file = "mariadb-1.1.8-cp312-cp312-win32.whl", hash = "sha256:6d96e2564079efdda7dfea059dfa12280257b9b90deaaf29c79052985d082d57"}, + {file = "mariadb-1.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:c62f6b849c07b2919750753167f42200fde68f9cbf240024d50a02760c439bf4"}, + {file = "mariadb-1.1.8-cp38-cp38-win32.whl", hash = "sha256:d4cd37945af2ac16fabaf3043dd96e016e33cad3c6c30787ab8d4f6ce54018b4"}, + {file = "mariadb-1.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:8e24362116d72ef4c2410d74a9455f45a5fe51151e145f0d4c8012a8cbd3955a"}, + {file = "mariadb-1.1.8-cp39-cp39-win32.whl", hash = "sha256:a542756bdc97b1f72bfb1f58a5246a14f22a694356a65c112a93018345eb93af"}, + {file = "mariadb-1.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:342312d23d535516e42576fb51fad81e03699645fc99df3157b06c4d6c326918"}, + {file = "mariadb-1.1.8.tar.gz", hash = "sha256:f73f1269dad5f14aee20b48ca56db7d90a51f2d5971a400c7d66e16b3976b4c9"}, +] + +[package.dependencies] +packaging = "*" + +[[package]] +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-git-committers-plugin-2" +version = "2.4.1" +description = "An MkDocs plugin to create a list of contributors on the page. The git-committers plugin will seed the template context with a list of GitHub or GitLab committers and other useful GIT info such as last modified date" +optional = false +python-versions = "<4,>=3.8" +files = [ + {file = "mkdocs_git_committers_plugin_2-2.4.1-py3-none-any.whl", hash = "sha256:ec9c1d81445606c471337d1c4a1782c643b7377077b545279dc18b86b7362c6d"}, + {file = "mkdocs_git_committers_plugin_2-2.4.1.tar.gz", hash = "sha256:ea1f80a79cedc42289e0b8e973276df04fb94f56e0ae3efc5385fb28547cf5cb"}, +] + +[package.dependencies] +gitpython = "*" +mkdocs = ">=1.0.3" +requests = "*" + +[[package]] +name = "mkdocs-material" +version = "9.5.49" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"}, + {file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, + {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, +] + +[[package]] +name = "mypy" +version = "1.7.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-boto3-dynamodb" +version = "1.35.74" +description = "Type annotations for boto3 DynamoDB 1.35.74 service generated with mypy-boto3-builder 8.5.0" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_boto3_dynamodb-1.35.74-py3-none-any.whl", hash = "sha256:b693b459abb1910cbb28f3a478ced8c6e6515f1bf136b45aca1a76b6146b5adb"}, + {file = "mypy_boto3_dynamodb-1.35.74.tar.gz", hash = "sha256:a815d044b8f5f4ba308ea3114916565fbd932fcaf218f8d0288b2840415f9c46"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-events" +version = "1.35.72" +description = "Type annotations for boto3 EventBridge 1.35.72 service generated with mypy-boto3-builder 8.5.0" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_boto3_events-1.35.72-py3-none-any.whl", hash = "sha256:4aacd448d2dae96acdbff0f91ed9548f4e94fe3be3b0536e3a417ad6521a5876"}, + {file = "mypy_boto3_events-1.35.72.tar.gz", hash = "sha256:866f48720b5cac92c9e30ab90454ce407cc2916a7423c22c6e2ed7ee042b9ce6"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-iam" +version = "1.35.61" +description = "Type annotations for boto3.IAM 1.35.61 service generated with mypy-boto3-builder 8.2.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_boto3_iam-1.35.61-py3-none-any.whl", hash = "sha256:2b26756fbf1ea3ad57546731b3f2c23780aa5448d1cb05dd80ba30bfc29ad581"}, + {file = "mypy_boto3_iam-1.35.61.tar.gz", hash = "sha256:cf307f7fb2404ceda7fda455f6d4cf3bdf57e12a3b16d27101db6223c59e6fe7"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-scheduler" +version = "1.35.0" +description = "Type annotations for boto3.EventBridgeScheduler 1.35.0 service generated with mypy-boto3-builder 7.26.0" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_boto3_scheduler-1.35.0-py3-none-any.whl", hash = "sha256:4ac9024a13d3db5ca2916dfed7586fb269323b09b05fa4a922afeaa9a8f20825"}, + {file = "mypy_boto3_scheduler-1.35.0.tar.gz", hash = "sha256:13786663c26dae4a0bac8822338ec99f33eb4b98e7986fa81bd6c3ae58799818"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-sesv2" +version = "1.35.79" +description = "Type annotations for boto3 SESV2 1.35.79 service generated with mypy-boto3-builder 8.6.3" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_boto3_sesv2-1.35.79-py3-none-any.whl", hash = "sha256:b155fe18884e67d96957717f9772821a8b0bf95e59c0d55268a4327dc20d4036"}, + {file = "mypy_boto3_sesv2-1.35.79.tar.gz", hash = "sha256:27bb00bc0c09f6abd7a9752ab004eda82a0fc2c6268fdbc920448ad1083f46c1"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-boto3-stepfunctions" +version = "1.35.68" +description = "Type annotations for boto3 SFN 1.35.68 service generated with mypy-boto3-builder 8.3.1" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy_boto3_stepfunctions-1.35.68-py3-none-any.whl", hash = "sha256:ceea974dd9f779f19042dc6ce6ac64955036be9b825512a6a33eecb7d682f43c"}, + {file = "mypy_boto3_stepfunctions-1.35.68.tar.gz", hash = "sha256:4abef0d339463ebe612836f42154a092e95eed025baca9a15be1286cc9a90434"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.12\""} + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "mysqlclient" +version = "2.2.0" +description = "Python interface to MySQL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mysqlclient-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:68837b6bb23170acffb43ae411e47533a560b6360c06dac39aa55700972c93b2"}, + {file = "mysqlclient-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5670679ff1be1cc3fef0fa81bf39f0cd70605ba121141050f02743eb878ac114"}, + {file = "mysqlclient-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:004fe1d30d2c2ff8072f8ea513bcec235fd9b896f70dad369461d0ad7e570e98"}, + {file = "mysqlclient-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c6b142836c7dba4f723bf9c93cc46b6e5081d65b2af807f400dda9eb85a16d0"}, + {file = "mysqlclient-2.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:955dba905a7443ce4788c63fdb9f8d688316260cf60b20ff51ac3b1c77616ede"}, + {file = "mysqlclient-2.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:530ece9995a36cadb6211b9787f0c9e05cdab6702549bdb4236af5e9b535ed6a"}, + {file = "mysqlclient-2.2.0.tar.gz", hash = "sha256:04368445f9c487d8abb7a878e3d23e923e6072c04a6c320f9e0dc8a82efba14e"}, +] + +[[package]] +name = "nh3" +version = "0.2.20" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"}, + {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"}, + {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"}, + {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"}, + {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"}, + {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"}, + {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.6" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pkginfo" +version = "1.12.0" +description = "Query metadata from sdists / bdists / installed packages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pkginfo-1.12.0-py3-none-any.whl", hash = "sha256:dcd589c9be4da8973eceffa247733c144812759aa67eaf4bbf97016a02f39088"}, + {file = "pkginfo-1.12.0.tar.gz", hash = "sha256:8ad91a0445a036782b9366ef8b8c2c50291f83a553478ba8580c73d3215700cf"}, +] + +[package.extras] +testing = ["pytest", "pytest-cov", "wheel"] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psycopg2" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, + {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, + {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, + {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, + {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, + {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, + {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, + {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, + {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, + {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, + {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, + {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, + {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.10.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, + {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.7.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"}, + {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +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 = "pymdown-extensions" +version = "10.12" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[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" +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.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python3-openid" +version = "3.2.0" +description = "OpenID support for modern servers and consumers." +optional = false +python-versions = "*" +files = [ + {file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"}, + {file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"}, +] + +[package.dependencies] +defusedxml = "*" + +[package.extras] +mysql = ["mysql-connector-python"] +postgresql = ["psycopg2"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "questionary" +version = "2.0.1" +description = "Python library to build pretty command line user prompts â­ī¸" +optional = false +python-versions = ">=3.8" +files = [ + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<=3.0.36" + +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[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 = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rich-click" +version = "1.8.5" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich_click-1.8.5-py3-none-any.whl", hash = "sha256:0fab7bb5b66c15da17c210b4104277cd45f3653a7322e0098820a169880baee0"}, + {file = "rich_click-1.8.5.tar.gz", hash = "sha256:a3eebe81da1c9da3c32f3810017c79bd687ff1b3fa35bfc9d8a3338797f1d1a1"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7" +typing_extensions = ">=4" + +[package.extras] +dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] +docs = ["markdown_include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] + +[[package]] +name = "s3transfer" +version = "0.10.4" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.8" +files = [ + {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, + {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, +] + +[package.dependencies] +botocore = ">=1.33.2,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "setuptools" +version = "70.3.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + +[[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" +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 = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + +[[package]] +name = "social-auth-app-django" +version = "5.4.2" +description = "Python Social Authentication, Django integration." +optional = false +python-versions = ">=3.8" +files = [ + {file = "social-auth-app-django-5.4.2.tar.gz", hash = "sha256:c8832c6cf13da6ad76f5613bcda2647d89ae7cfbc5217fadd13477a3406feaa8"}, + {file = "social_auth_app_django-5.4.2-py3-none-any.whl", hash = "sha256:0c041a31707921aef9a930f143183c65d8c7b364381364a50f3f7c6fcc9d62f6"}, +] + +[package.dependencies] +Django = ">=3.2" +social-auth-core = ">=4.4.1" + +[[package]] +name = "social-auth-core" +version = "4.5.4" +description = "Python social authentication made simple." +optional = false +python-versions = ">=3.8" +files = [ + {file = "social-auth-core-4.5.4.tar.gz", hash = "sha256:d3dbeb0999ffd0e68aa4bd73f2ac698a18133fd11b3fc890e1366f18c8889fac"}, + {file = "social_auth_core-4.5.4-py3-none-any.whl", hash = "sha256:33cf970a623c442376f9d4a86fb187579e4438649daa5b5be993d05e74d7b2db"}, +] + +[package.dependencies] +cryptography = ">=1.4" +defusedxml = ">=0.5.0rc1" +oauthlib = ">=1.0.3" +PyJWT = ">=2.7.0" +python3-openid = ">=3.0.10" +requests = ">=2.9.1" +requests-oauthlib = ">=0.6.1" + +[package.extras] +all = ["cryptography (>=2.1.1)", "python3-saml (>=1.5.0)"] +allpy3 = ["cryptography (>=2.1.1)", "python3-saml (>=1.5.0)"] +azuread = ["cryptography (>=2.1.1)"] +saml = ["python3-saml (>=1.5.0)"] + +[[package]] +name = "sqlparse" +version = "0.5.3" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "stripe" +version = "10.12.0" +description = "Python bindings for the Stripe API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "stripe-10.12.0-py2.py3-none-any.whl", hash = "sha256:0c79c1f3a844533c8d30cc283b43afb622aaa402539fca19167a9004fea3471c"}, + {file = "stripe-10.12.0.tar.gz", hash = "sha256:5abec44548d3814bc1e070aa1852bcb3fc5cc029e947c0f733156eb1f8c87030"}, +] + +[package.dependencies] +requests = {version = ">=2.20", markers = "python_version >= \"3.0\""} +typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""} + +[[package]] +name = "tblib" +version = "3.0.0" +description = "Traceback serialization library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tblib-3.0.0-py3-none-any.whl", hash = "sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129"}, + {file = "tblib-3.0.0.tar.gz", hash = "sha256:93622790a0a29e04f0346458face1e144dc4d32f493714c6c3dff82a4adb77e6"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "twine" +version = "6.0.1" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "twine-6.0.1-py3-none-any.whl", hash = "sha256:9c6025b203b51521d53e200f4a08b116dee7500a38591668c6a6033117bdc218"}, + {file = "twine-6.0.1.tar.gz", hash = "sha256:36158b09df5406e1c9c1fb8edb24fc2be387709443e7376689b938531582ee27"}, +] + +[package.dependencies] +keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = "*" +pkginfo = ">=1.8.1" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=15.1)"] + +[[package]] +name = "types-awscrt" +version = "0.23.6" +description = "Type annotations and code completion for awscrt" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_awscrt-0.23.6-py3-none-any.whl", hash = "sha256:fbf9c221af5607b24bf17f8431217ce8b9a27917139edbc984891eb63fd5a593"}, + {file = "types_awscrt-0.23.6.tar.gz", hash = "sha256:405bce8c281f9e7c6c92a229225cc0bf10d30729a6a601123213389bd524b8b1"}, +] + +[[package]] +name = "types-cffi" +version = "1.16.0.20240331" +description = "Typing stubs for cffi" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-cffi-1.16.0.20240331.tar.gz", hash = "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee"}, + {file = "types_cffi-1.16.0.20240331-py3-none-any.whl", hash = "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0"}, +] + +[package.dependencies] +types-setuptools = "*" + +[[package]] +name = "types-docutils" +version = "0.21.0.20241128" +description = "Typing stubs for docutils" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_docutils-0.21.0.20241128-py3-none-any.whl", hash = "sha256:e0409204009639e9b0bf4521eeabe58b5e574ce9c0db08421c2ac26c32be0039"}, + {file = "types_docutils-0.21.0.20241128.tar.gz", hash = "sha256:4dd059805b83ac6ec5a223699195c4e9eeb0446a4f7f2aeff1759a4a7cc17473"}, +] + +[[package]] +name = "types-markdown" +version = "3.7.0.20241204" +description = "Typing stubs for Markdown" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_Markdown-3.7.0.20241204-py3-none-any.whl", hash = "sha256:f96146c367ea9c82bfe9903559d72706555cc2a1a3474c58ebba03b418ab18da"}, + {file = "types_markdown-3.7.0.20241204.tar.gz", hash = "sha256:ecca2b25cd23163fd28ed5ba34d183d731da03e8a5ed3a20b60daded304c5410"}, +] + +[[package]] +name = "types-protobuf" +version = "5.29.1.20241207" +description = "Typing stubs for protobuf" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_protobuf-5.29.1.20241207-py3-none-any.whl", hash = "sha256:92893c42083e9b718c678badc0af7a9a1307b92afe1599e5cba5f3d35b668b2f"}, + {file = "types_protobuf-5.29.1.20241207.tar.gz", hash = "sha256:2ebcadb8ab3ef2e3e2f067e0882906d64ba0dc65fc5b0fd7a8b692315b4a0be9"}, +] + +[[package]] +name = "types-psycopg2" +version = "2.9.21.20241019" +description = "Typing stubs for psycopg2" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-psycopg2-2.9.21.20241019.tar.gz", hash = "sha256:bca89b988d2ebd19bcd08b177d22a877ea8b841decb10ed130afcf39404612fa"}, + {file = "types_psycopg2-2.9.21.20241019-py3-none-any.whl", hash = "sha256:44d091e67732d16a941baae48cd7b53bf91911bc36888652447cf1ef0c1fb3f6"}, +] + +[[package]] +name = "types-pycurl" +version = "7.45.4.20241216" +description = "Typing stubs for pycurl" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_pycurl-7.45.4.20241216-py3-none-any.whl", hash = "sha256:d50eb912216ae2bab4f581bf44cd013f24002f001077a38fcc662a25b525342c"}, + {file = "types_pycurl-7.45.4.20241216.tar.gz", hash = "sha256:ce5ad0978e09ef43ee97b54ac68c183eda27ee0c981f70ec3f1b867ff07611f4"}, +] + +[[package]] +name = "types-pygments" +version = "2.18.0.20240506" +description = "Typing stubs for Pygments" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-Pygments-2.18.0.20240506.tar.gz", hash = "sha256:4b4c37812c87bbde687dbf27adf5bac593745a321e57f678dbc311571ba2ac9d"}, + {file = "types_Pygments-2.18.0.20240506-py3-none-any.whl", hash = "sha256:11c90bc1737c9af55e5569558b88df7c2233e12325cb516215f722271444e91d"}, +] + +[package.dependencies] +types-docutils = "*" +types-setuptools = "*" + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + +[[package]] +name = "types-pytz" +version = "2024.2.0.20241003" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-pytz-2024.2.0.20241003.tar.gz", hash = "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44"}, + {file = "types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240917" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"}, + {file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"}, +] + +[[package]] +name = "types-requests" +version = "2.32.0.20241016" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, + {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "types-s3transfer" +version = "0.10.4" +description = "Type annotations and code completion for s3transfer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:22ac1aabc98f9d7f2928eb3fb4d5c02bf7435687f0913345a97dd3b84d0c217d"}, + {file = "types_s3transfer-0.10.4.tar.gz", hash = "sha256:03123477e3064c81efe712bf9d372c7c72f2790711431f9baa59cf96ea607267"}, +] + +[[package]] +name = "types-setuptools" +version = "75.6.0.20241126" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_setuptools-75.6.0.20241126-py3-none-any.whl", hash = "sha256:aaae310a0e27033c1da8457d4d26ac673b0c8a0de7272d6d4708e263f2ea3b9b"}, + {file = "types_setuptools-75.6.0.20241126.tar.gz", hash = "sha256:7bf25ad4be39740e469f9268b6beddda6e088891fa5a27e985c6ce68bf62ace0"}, +] + +[[package]] +name = "types-six" +version = "1.17.0.20241205" +description = "Typing stubs for six" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_six-1.17.0.20241205-py3-none-any.whl", hash = "sha256:a4947c2bdcd9ab69d44466a533a15839ff48ddc27223615cb8145d73ab805bc2"}, + {file = "types_six-1.17.0.20241205.tar.gz", hash = "sha256:1f662347a8f3b2bf30517d629d82f591420df29811794b0bf3804e14d716f6e0"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "typos" +version = "1.28.4" +description = "Source Code Spelling Correction" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typos-1.28.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0bb4945868432848bdb378137411f0c5d6f7f763e4da64b84b037ad2392b45f8"}, + {file = "typos-1.28.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54d507db5439e65ebb36a15551ba0fd23d317ed6d4212d0b866a5310a14d8841"}, + {file = "typos-1.28.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bacfb01a2ab60b146f1412252f327e58e32a430613a761d76dbcc6e275ecffe3"}, + {file = "typos-1.28.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3bc312a10df63211b4f8730d551bc086f71ec5fb7a0a587a50f16c3902edf76"}, + {file = "typos-1.28.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f605b3bb8c928cc0a0d46b29335d400630d43da0a9977bc890987a6cc175420a"}, + {file = "typos-1.28.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d854c5f854304efb959d7fe56fef5720163738687a6db6232bbd951ee2190167"}, + {file = "typos-1.28.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce87ddde847c535354dbe036691473fc6029f2c895f47340167874185b14bb29"}, + {file = "typos-1.28.4-py3-none-win32.whl", hash = "sha256:92a1a2eaa88f682f9f23152d3f20c932d64e18390819e69b2d006c3e83afe3e3"}, + {file = "typos-1.28.4-py3-none-win_amd64.whl", hash = "sha256:23dbb854917e4d8eaba6ff364d4849d09fc70faeb0f88bc35c814b622b6d045a"}, + {file = "typos-1.28.4.tar.gz", hash = "sha256:7afd8ad79ab8b84f7adb12350d5630abc5e061c8a76802ddbc29eea256689600"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.28.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<4.0" +content-hash = "afe23a783d3bb45217d9964370ad273a2afff97ff28fd23844819800ca359cff" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d9620a5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,183 @@ +[tool.poetry] +name = "Strelix-Core" +version = "0.0.1" +description = "github.com/Strelix/core" +authors = ["TreyWW"] +readme = "README.md" +package-mode = true + +packages = [ + { include = "core", from = "src" }, + { include = "billing", from = "src" }, +] + +[project] +name = "strelix-core" +version = "0.0.1" +description = "Strelix Core" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "MIT" } +authors = [ + { name = "TreyWW", email = "trey@strelix.org" }, +] + +keywords = ["identity", "authentication", "strelix", "myfinances"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + + +[project.urls] +Homepage = "https://github.com/Strelix/core" + +[tool.poetry.dependencies] +python = ">=3.10,<4.0" +bleach = "6.1.0" # used for HTML sanitation +tblib = "^3.0.0" # Exceptions and Tracebacks +django = "^5.0.7" + +setuptools = "^70.1.1" +[tool.poetry.group.dev.dependencies] +black = "^24.3.0" +djlint = "^1.34.1" +coverage = "^7.4.4" +pre-commit = "^3.7.0" +bump-my-version = "^0.19.3" +mypy = "1.7.1" +django-stubs = { version = "4.2.7" } +django-stubs-ext = { version = "4.2.7" } +djangorestframework-stubs = { extras = ["compatible-mypy"], version = "^3.14.5" } +djangorestframework = "^3.14.0" +typos = "^1.20.3" +types-python-dateutil = "^2.9.0.20240906" +types-markdown = "^3.7.0.20240822" +types-pygments = "^2.18.0.20240506" +types-cffi = "^1.16.0.20240331" +types-protobuf = "^5.27.0.20240907" +types-psycopg2 = "^2.9.21.20240819" +types-pycurl = "^7.45.3.20240421" +types-six = "^1.16.21.20240513" +mkdocs = "^1.5.3" +mkdocs-material = "^9.5.15" +mkdocs-git-committers-plugin-2 = "^2.3.0" +build = "^1.2.2.post1" +twine = "^6.0.1" # Package creation for PyPi +django-storages = "1.14.2" # AWS S3 +boto3 = "1.34.76" # AWS +pillow = "10.3.0" # Images +stripe = "^10.8.0" # Payments +django-environ = "0.11.2" # ENV Vars +django-ratelimit = "4.1.0" # Rate Limiting +django-login-required-middleware = "0.9.0" # todo: remove soon and use custom +social-auth-app-django = "^5.4.2" # social logins | todo: may be able to remove? +django-silk = "^5.3.2" # request logging and performance viewing +[tool.poetry.group.mypy.dependencies] +mypy = "1.7.1" +django-stubs = { version = "4.2.7" } +django-stubs-ext = { version = "4.2.7" } +djangorestframework-stubs = { extras = ["compatible-mypy"], version = "^3.14.5" }# +boto3-stubs = { extras = [ + "sesv2", + "events", + "scheduler", + "dynamodb", + "iam", + "stepfunctions", +], version = "^1.34.76" } + +[tool.poetry.group.mysql] +optional = true + +[tool.poetry.group.mysql.dependencies] +mysqlclient = "2.2.0" +mariadb = "1.1.8" + +[tool.poetry.group.postgres] +optional = true + +[tool.poetry.group.postgres.dependencies] +psycopg2 = "2.9.9" + +[tool.djlint] +profile = "django" +max_line_length = 110 + +[tool.black] +line-length = 140 + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.bumpversion] +current_version = "0.0.1" +commit = true +commit_args = "-s" +tag = true +tag_name = "v{new_version}" +sign_tags = true +allow_dirty = true +parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(\\.(?Ppost)\\d+\\.dev\\d+)?" +serialize = [ + "{major}.{minor}.{patch}.{dev}{$PR_NUMBER}.dev{distance_to_latest_tag}", + "{major}.{minor}.{patch}" +] + +message = "Version updated from {current_version} to {new_version}" + +[[tool.bumpversion.files]] +filename = "backend/__init__.py" + +[[tool.bumpversion.files]] +filename = "package.json" +search = '"version": "{current_version}"' +replace = '"version": "{new_version}"' + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version = "{current_version}"' +replace = 'version = "{new_version}"' + +[tool.mypy] +plugins = [ + "mypy_django_plugin.main", + "mypy_drf_plugin.main" +] +disable_error_code = ["annotation-unchecked"] +explicit_package_bases = true +incremental = false +exclude = [ + "venv", + ".venv", + "env", + "ENV", + "ENV_DIR", + "env.bak", + "venv.bak", +] + +[tool.django-stubs] +django_settings_module = "settings.settings" + +[[tool.mypy.overrides]] +module = [ + "django.core.cache.backends.redis", + "django_ratelimit.core", + "django_ratelimit.decorators", + "django_ratelimit.exceptions", + "django_ratelimit", + "environ", + "login_required", + "emails", + "step_functions", + "django_components", + "drf_yasg", + "drf_yasg.utils", + "drf_yasg.views", +] +ignore_missing_imports = true diff --git a/src/billing/__init__.py b/src/billing/__init__.py new file mode 100644 index 0000000..2088f7c --- /dev/null +++ b/src/billing/__init__.py @@ -0,0 +1 @@ +from . import apps, billing_settings diff --git a/src/billing/admin.py b/src/billing/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/src/billing/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/billing/apps.py b/src/billing/apps.py new file mode 100644 index 0000000..367d058 --- /dev/null +++ b/src/billing/apps.py @@ -0,0 +1,21 @@ +import importlib + +from django.apps import AppConfig +from django.conf import settings + +class BillingConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "billing" + + def ready(self): + from . import signals + + self._load_project_signals() + + def _load_project_signals(self): + for app_name in getattr(settings, "INSTALLED_APPS", []): + try: + module_name = f"{app_name}.billing.signals" + importlib.import_module(module_name) + except ModuleNotFoundError: + pass diff --git a/src/billing/billing_settings.py b/src/billing/billing_settings.py new file mode 100644 index 0000000..5e50df1 --- /dev/null +++ b/src/billing/billing_settings.py @@ -0,0 +1,43 @@ +import stripe + +from core.utils.settings_helper import get_var + +STRIPE_TEST_SECRET_KEY = get_var("STRIPE_TEST_SECRET_KEY") +STRIPE_LIVE_SECRET_KEY = get_var("STRIPE_LIVE_SECRET_KEY") +STRIPE_WEBHOOK_ENDPOINT_SECRET = get_var("STRIPE_WEBHOOK_ENDPOINT_SECRET") + +STRIPE_MAIN_API_KEY = STRIPE_LIVE_SECRET_KEY if STRIPE_LIVE_SECRET_KEY else STRIPE_TEST_SECRET_KEY + +STRIPE_LIVE_MODE = True if STRIPE_LIVE_SECRET_KEY else False + +stripe.api_key = STRIPE_MAIN_API_KEY + +NO_SUBSCRIPTION_PLAN_DENY_VIEW_NAMES: set[str] = { + "clients:create", + "file_storage:upload:start_batch", + "file_storage:upload:end_batch", + "file_storage:upload:add_to_batch", + "file_storage:upload:dashboard", + # "finance:invoices:single:manage_access", + "finance:invoices:single:manage_access create", + # "finance:invoices:single:manage_access delete", + "finance:invoices:single:edit", + "finance:invoices:single:create", + "finance:invoices:recurring:create", + "finance:invoices:recurring:edit", + # APIS + "teams:invite", + "teams:create", + "receipts:edit", + "receipts:new", + "finance:invoices:single:edit", + "finance:invoices:single:edit discount", + "finance:invoices:recurring:generate next invoice", + "finance:invoices:recurring:edit", + "finance:invoices:create:set_destination from", + "finance:invoices:create:set_destination to", + "finance:invoices:create:services add", + "products:create", + "public:clients:create", + "public:invoices:create", +} diff --git a/src/billing/data/__init__.py b/src/billing/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/billing/data/default_usage_plans.py b/src/billing/data/default_usage_plans.py new file mode 100644 index 0000000..0f50ec7 --- /dev/null +++ b/src/billing/data/default_usage_plans.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +from dataclasses import dataclass +from decimal import Decimal +from typing import Literal + + +@dataclass +class DefaultFeature: + """ + Single Product, e.g. "invoices-created" + This is a stripe "PRICE" (part of a PRODUCT) + """ + + slug: str # Consistent slug across plans + description: str + max_limit_per_month: int + subscription_plan: DefaultSubscriptionPlan + + +@dataclass +class DefaultFeatureGroup: + """ + Group of products, e.g. "Invoices" + """ + + name: str + items: list[DefaultFeature] + + +@dataclass +class DefaultSubscriptionPlan: + """ + This is a Stripe PRODUCT + """ + + name: str + price_per_month: int + description: str + + +# Default subscription plans +free_plan = DefaultSubscriptionPlan( + name="Free Trial", + price_per_month=0, + description="Try out MyFinances", +) + +starter_plan = DefaultSubscriptionPlan( + name="Starter", + price_per_month=5, + description="For small businesses that need limited features", +) + +growth_plan = DefaultSubscriptionPlan( + name="Growth", + price_per_month=10, + description="For growing businesses that need a little extra", +) + +# enterprise_plan = DefaultSubscriptionPlan( +# name="Enterprise", +# price_per_month=-1, +# description="Additional customization for your ideal business", +# ) + +default_subscription_plans: list[DefaultSubscriptionPlan] = [free_plan, starter_plan, growth_plan] + +# Default usage plans +default_usage_plans: list[DefaultFeatureGroup] = [ + DefaultFeatureGroup( + "invoices", + [ + # region "invoices-created" + DefaultFeature( + slug="invoices-created", + description="Amount of invoices created per month", + max_limit_per_month=10, + subscription_plan=free_plan, + ), + DefaultFeature( + slug="invoices-created", + description="Amount of invoices created per month (starter plan)", + max_limit_per_month=500, + subscription_plan=starter_plan, + ), + DefaultFeature( + slug="invoices-created", + description="Amount of invoices created per month", + max_limit_per_month=-1, + subscription_plan=growth_plan, + ), + # endregion "invoices-created" + # region "invoices-sent-via-schedule" + DefaultFeature( + slug="invoices-sent-via-schedule", + description="Amount of invoices sent from a schedule per month", + max_limit_per_month=1, + subscription_plan=free_plan, + ), + DefaultFeature( + slug="invoices-sent-via-schedule", + description="Amount of invoices sent from a schedule per month", + max_limit_per_month=50, + subscription_plan=starter_plan, + ), + DefaultFeature( + slug="invoices-sent-via-schedule", + description="Amount of invoices sent from a schedule per month", + max_limit_per_month=-1, + subscription_plan=growth_plan, + ), + # endregion "invoices-sent-via-schedule" + ], + ), + DefaultFeatureGroup( + "teams", + [ + # region "organization-access" + DefaultFeature( + slug="organization-access", + description="Amount of invoices created per month (starter plan)", + max_limit_per_month=1, + subscription_plan=starter_plan, + ), + DefaultFeature( + slug="organization-access", + description="Amount of invoices created per month", + max_limit_per_month=1, + subscription_plan=growth_plan, + ), + # endregion "organization-access" + ], + ), +] diff --git a/src/billing/decorators.py b/src/billing/decorators.py new file mode 100644 index 0000000..ed755de --- /dev/null +++ b/src/billing/decorators.py @@ -0,0 +1,32 @@ +from django.contrib import messages +from django.http import HttpResponseRedirect +from django.shortcuts import redirect, render +from django.urls import reverse + +from billing.service.entitlements import has_entitlement, get_entitlements + + +def has_entitlements_called_from_backend_handler(entitlements: list[str] | str, htmx_api: bool = False): + def decorator(view_func): + def wrapper_func(request, *args, **kwargs): + user_does_have_entitlements: bool + if isinstance(entitlements, (list, set)): + users_entitlements = get_entitlements(request.actor) + user_does_have_entitlements = all(entitlement in users_entitlements for entitlement in entitlements) + else: + user_does_have_entitlements = has_entitlement(request.actor, entitlements) + + if user_does_have_entitlements: + return view_func(request, *args, **kwargs) + else: + messages.warning(request, f"Your plan unfortunately doesn't include this feature.") + + if htmx_api: + return render(request, "base/toast.html", {"autohide": False}) + elif request.htmx: + return HttpResponseRedirect(reverse("billing:dashboard")) + return redirect("billing:dashboard") + + return wrapper_func + + return decorator diff --git a/src/billing/management/__init__.py b/src/billing/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/billing/management/commands/__init__.py b/src/billing/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/billing/management/commands/stripe.py b/src/billing/management/commands/stripe.py new file mode 100644 index 0000000..136e531 --- /dev/null +++ b/src/billing/management/commands/stripe.py @@ -0,0 +1,43 @@ +from django.core.management.base import BaseCommand +from django.core.cache import cache +import stripe + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + "action", + type=str, + help="help, create_entitlements", + ) + + def handle(self, *args, **kwargs): + action = kwargs.get("action") + + match action: + case "create_entitlements": + for entitlement in [ + "Receipts", + "File Storage", + "Organizations", + "Invoice Reminders", + "API Access", + "Emails", + "Advanced Onboarding", + "Basic Onboarding", + "Invoice Schedules", + "Invoices", + "Customers", + ]: + try: + stripe.entitlements.Feature.create(name=entitlement, lookup_key=entitlement.lower().replace(" ", "-")) + print(f"Created entitlement: {entitlement}") + except stripe.error.InvalidRequestError: + print(f"Entitlement already exists: {entitlement}") + case (_, "help"): + print( + """ + Available actions: + - create_entitlements: This will create all entitlements that you don't have + """ + ) diff --git a/src/billing/middleware.py b/src/billing/middleware.py new file mode 100644 index 0000000..bf02173 --- /dev/null +++ b/src/billing/middleware.py @@ -0,0 +1,50 @@ +from django.contrib import messages +from django.shortcuts import redirect, render +from django.urls import resolve + +from core.types.requests import WebRequest +from billing.billing_settings import NO_SUBSCRIPTION_PLAN_DENY_VIEW_NAMES +from billing.models import UserSubscription + + +# middleware to check if user is subscribed to a plan yet + + +class CheckUserSubScriptionMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: WebRequest): + if not request.user.is_authenticated: + return self.get_response(request) + + if request.team: + # todo: handle organization billing + return self.get_response(request) + + subscription: UserSubscription | None = ( + UserSubscription.filter_by_owner(request.actor).filter(end_date__isnull=True).prefetch_related("subscription_plan").first() + ) + request.users_subscription = subscription + + resolver_match = resolve(request.path_info) + + view_name = resolver_match.view_name + + if view_name not in NO_SUBSCRIPTION_PLAN_DENY_VIEW_NAMES: + return self.get_response(request) + + if not subscription: + print("[BILLING] [MIDDLEWARE] User doesn't have an active subscription.") + messages.warning( + request, + """ + You currently are not subscribed to a plan. If you think this is a mistake scroll down and + press "Refetch" or contact support at + support@strelix.org.""", + ) + + if request.htmx: + return render(request, "base/toast.html", {"autohide": False}) + return redirect("billing:dashboard") + return self.get_response(request) diff --git a/src/billing/migrations/0001_initial.py b/src/billing/migrations/0001_initial.py new file mode 100644 index 0000000..3c6fee1 --- /dev/null +++ b/src/billing/migrations/0001_initial.py @@ -0,0 +1,192 @@ +# Generated by Django 5.1.4 on 2024-12-21 22:26 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="BillingUsage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("event_name", models.CharField(max_length=100)), + ( + "event_type", + models.CharField( + choices=[("usage", "Metered Usage")], + default="usage", + max_length=20, + ), + ), + ("quantity", models.PositiveSmallIntegerField(default=1)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("processed_at", models.DateTimeField(blank=True, null=True)), + ("processed", models.BooleanField(default=False)), + ( + "stripe_unique_usage_identifier", + models.CharField(blank=True, max_length=100, null=True), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="PlanFeature", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("slug", models.CharField(max_length=100)), + ( + "stripe_price_id", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "description", + models.TextField(blank=True, max_length=500, null=True), + ), + ("max_limit_per_month", models.IntegerField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name="PlanFeatureGroup", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ], + ), + migrations.CreateModel( + name="StripeCheckoutSession", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ( + "stripe_session_id", + models.CharField( + blank=True, max_length=100, null=True, unique=True + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="StripeWebhookEvent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("event_id", models.CharField(max_length=100, unique=True)), + ("event_type", models.CharField(max_length=100)), + ("data", models.JSONField()), + ("raw_event", models.JSONField()), + ], + ), + migrations.CreateModel( + name="SubscriptionPlan", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50, unique=True)), + ( + "price_per_month", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ( + "description", + models.TextField(blank=True, max_length=500, null=True), + ), + ( + "stripe_product_id", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "stripe_price_id", + models.CharField(blank=True, max_length=100, null=True), + ), + ], + ), + migrations.CreateModel( + name="UserSubscription", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ( + "stripe_subscription_id", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "custom_subscription_price_per_month", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ("start_date", models.DateTimeField(auto_now_add=True)), + ("end_date", models.DateTimeField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/billing/migrations/0002_initial.py b/src/billing/migrations/0002_initial.py new file mode 100644 index 0000000..61d0c63 --- /dev/null +++ b/src/billing/migrations/0002_initial.py @@ -0,0 +1,155 @@ +# Generated by Django 5.1.4 on 2024-12-21 22:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("billing", "0001_initial"), + ("core", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="billingusage", + name="organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + migrations.AddField( + model_name="billingusage", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="planfeature", + name="group", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="features", + to="billing.planfeaturegroup", + ), + ), + migrations.AddField( + model_name="stripecheckoutsession", + name="features", + field=models.ManyToManyField( + related_name="checkout_sessions", to="billing.planfeature" + ), + ), + migrations.AddField( + model_name="stripecheckoutsession", + name="organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + migrations.AddField( + model_name="stripecheckoutsession", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="stripecheckoutsession", + name="plan", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="checkout_sessions", + to="billing.subscriptionplan", + ), + ), + migrations.AddField( + model_name="planfeature", + name="subscription_plan", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="features", + to="billing.subscriptionplan", + ), + ), + migrations.AddField( + model_name="usersubscription", + name="organization", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + migrations.AddField( + model_name="usersubscription", + name="subscription_plan", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="billing.subscriptionplan", + ), + ), + migrations.AddField( + model_name="usersubscription", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddConstraint( + model_name="billingusage", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="billing_billingusage_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="stripecheckoutsession", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="billing_stripecheckoutsession_check_user_or_organization", + ), + ), + migrations.AddConstraint( + model_name="usersubscription", + constraint=models.CheckConstraint( + condition=models.Q( + models.Q(("organization__isnull", False), ("user__isnull", True)), + models.Q(("organization__isnull", True), ("user__isnull", False)), + _connector="OR", + ), + name="billing_usersubscription_check_user_or_organization", + ), + ), + ] diff --git a/src/billing/migrations/__init__.py b/src/billing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/billing/models.py b/src/billing/models.py new file mode 100644 index 0000000..75b2787 --- /dev/null +++ b/src/billing/models.py @@ -0,0 +1,117 @@ +from uuid import uuid4 + +from django.db import models + +from core.models import OwnerBase + +from django.utils import timezone + +from django.utils.timezone import now as timezone_now + + +class SubscriptionPlan(models.Model): + """ + Subscription plans available for users. + """ + + name = models.CharField(max_length=50, unique=True) + price_per_month = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + description = models.TextField(max_length=500, null=True, blank=True) + stripe_product_id = models.CharField(max_length=100, null=True, blank=True) + stripe_price_id = models.CharField(max_length=100, null=True, blank=True) + + +def __str__(self): + return f"{self.name} - {self.price_per_month or 'free' if self.price_per_month != -1 else 'custom'}" + + +class UserSubscription(OwnerBase): + """ + Track which subscription plan a user is currently subscribed to. + """ + + uuid = models.UUIDField(unique=True, default=uuid4) + subscription_plan = models.ForeignKey(SubscriptionPlan, on_delete=models.SET_NULL, null=True) + stripe_subscription_id = models.CharField(max_length=100, null=True, blank=True) + # Custom price only used for enterprise or negotiated plans + custom_subscription_price_per_month = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + + start_date = models.DateTimeField(auto_now_add=True) + end_date = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"{self.owner} - {self.subscription_plan.name} ({self.start_date} to {self.end_date or 'ongoing'})" + + @property + def has_ended(self): + return bool(self.end_date) + + @property + def get_price(self): + return self.custom_subscription_price_per_month or self.subscription_plan.price_per_month or "0.00" + + def end_now(self): + self.end_date = timezone.now() + self.save() + return self + + +class PlanFeatureGroup(models.Model): + name = models.CharField(max_length=50) # E.g. 'invoices' + + +class PlanFeature(models.Model): + """ + Details related to certain features. E.g. "emails sent", we can allow site admins to change prices, units, and customise their + billing + """ + + slug = models.CharField(max_length=100) + stripe_price_id = models.CharField(max_length=100, null=True, blank=True) + description = models.TextField(max_length=500, null=True, blank=True) + + max_limit_per_month = models.IntegerField(null=True, blank=True) + + subscription_plan = models.ForeignKey(SubscriptionPlan, on_delete=models.CASCADE, related_name="features") + group = models.ForeignKey(PlanFeatureGroup, on_delete=models.CASCADE, related_name="features") + + def __str__(self): + return f"{self.slug} - subscription id: {self.subscription_plan_id}" + + +class StripeWebhookEvent(models.Model): + event_id = models.CharField(max_length=100, unique=True) + event_type = models.CharField(max_length=100) # e.g. 'customer.subscription.created' + data = models.JSONField() + raw_event = models.JSONField() + + +class StripeCheckoutSession(OwnerBase): + uuid = models.UUIDField(unique=True, default=uuid4) + + stripe_session_id = models.CharField(max_length=100, unique=True, blank=True, null=True) + plan = models.ForeignKey(SubscriptionPlan, on_delete=models.CASCADE, related_name="checkout_sessions") + features = models.ManyToManyField(PlanFeature, related_name="checkout_sessions") + + +class BillingUsage(OwnerBase): + EVENT_TYPES = ( + ("usage", "Metered Usage"), + # ("storage", "Storage"), + ) + + event_name = models.CharField(max_length=100) # e.g. 'invoices-created' + event_type = models.CharField(max_length=20, choices=EVENT_TYPES, default="usage") + quantity = models.PositiveSmallIntegerField(default=1) # e.g. 1 + + created_at = models.DateTimeField(auto_now_add=True) + + processed_at = models.DateTimeField(null=True, blank=True) + processed = models.BooleanField(default=False) + stripe_unique_usage_identifier = models.CharField(max_length=100, null=True, blank=True) + + def set_processed(self, processed_time): + self.processed = True + self.processed_at = processed_time or timezone_now() + self.save() + return self diff --git a/src/billing/service/__init__.py b/src/billing/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/billing/service/checkout_completed.py b/src/billing/service/checkout_completed.py new file mode 100644 index 0000000..02b7e12 --- /dev/null +++ b/src/billing/service/checkout_completed.py @@ -0,0 +1,48 @@ +import stripe + +from core.utils.calendar import timezone_now +from billing.models import StripeCheckoutSession, StripeWebhookEvent, UserSubscription + + +def checkout_completed(webhook_event: StripeWebhookEvent): + event_data: stripe.checkout.Session = webhook_event.data["object"] + + stripe_session_obj = StripeCheckoutSession.objects.filter( + uuid=event_data.metadata.get("dj_checkout_uuid", "doesn't_exist") if event_data.metadata else "doesn't_exist" + ).first() # type: ignore[misc] + + if not stripe_session_obj: + print("No matching session object found.") + return + + completed_with_session_object(stripe_session_obj, event_data) + + +def completed_with_session_object(stripe_session_obj: StripeCheckoutSession, event_data: stripe.checkout.Session) -> None: + # Fetch current active subscriptions based on the owner (user or organization) + user_current_plans = UserSubscription.filter_by_owner(owner=stripe_session_obj.owner).filter(end_date__isnull=True) + + # Get plan ID from metadata + stripe_plan_id = event_data.metadata.get("dj_subscription_plan_id", None) if event_data.metadata else None + if not stripe_plan_id: + print("No subscription plan ID found in metadata.") + return + + # Cancel existing subscriptions except the one in the metadata + for current_plan in user_current_plans: + if current_plan.subscription_plan.id != stripe_plan_id: # Fix: Using `subscription_plan.id` + stripe.Subscription.modify(current_plan.stripe_subscription_id, cancel_at_period_end=True) # type: ignore[arg-type] + current_plan.end_date = timezone_now() + current_plan.save() + + # Create new subscription if the user doesn't have it + if not user_current_plans.filter(subscription_plan__id=stripe_plan_id).exists(): # Fix: Using `subscription_plan__id` + UserSubscription.objects.create( + owner=stripe_session_obj.owner, + subscription_plan_id=stripe_plan_id, + stripe_subscription_id=event_data.subscription, + ) + + # Expire the checkout session + stripe.checkout.Session.expire(stripe_session_obj.stripe_session_id) # type: ignore[arg-type] + stripe_session_obj.delete() diff --git a/src/billing/service/entitlements.py b/src/billing/service/entitlements.py new file mode 100644 index 0000000..66e6b59 --- /dev/null +++ b/src/billing/service/entitlements.py @@ -0,0 +1,55 @@ +import stripe.entitlements +from django.core.cache import cache +from django.core.cache.backends.redis import RedisCacheClient + +from core.models import User, Organization +from billing.models import StripeWebhookEvent +from billing.service.get_user import get_actor_from_stripe_customer + +cache: RedisCacheClient = cache + + +def entitlements_updated_via_stripe_webhook(webhook_event: StripeWebhookEvent) -> None: + data: stripe.entitlements.ActiveEntitlementSummary = webhook_event.data["object"] + actor = get_actor_from_stripe_customer(data["customer"]) + + if not actor: + print("No actor found for customer.") + return + + # Re-fetch and update the entitlements for the actor (User or Organization) + update_user_entitlements(actor) + + +def update_user_entitlements(actor: User | Organization) -> list[str]: + if not actor.stripe_customer_id: + return [] + + entitlements = stripe.entitlements.ActiveEntitlement.list(customer=actor.stripe_customer_id, limit=25).data + + entitlement_names = [entitlement.lookup_key for entitlement in entitlements] + + actor.entitlements = entitlement_names + actor.save(update_fields=["entitlements"]) + + cache_actor_type = "user" if isinstance(actor, User) else "org" + + cache.set(f"myfinances:entitlements:{cache_actor_type}:{actor.id}", entitlement_names, timeout=3600) + + return entitlement_names + + +def get_entitlements(actor: User | Organization, avoid_cache=False) -> list[str]: + cache_key = "user" if isinstance(actor, User) else "org" + + if not avoid_cache and (cached_entitlements := cache.get(f"myfinances:entitlements:{cache_key}:{actor.id}", default=[])): + return cached_entitlements + return update_user_entitlements(actor) + + +def has_entitlement(actor: User | Organization, entitlement: str) -> bool: + return entitlement in get_entitlements(actor) + + +def has_entitlements(actor: User | Organization, entitlements: list[str]) -> bool: + return all(entitlement in entitlements for entitlement in get_entitlements(actor)) diff --git a/src/billing/service/get_user.py b/src/billing/service/get_user.py new file mode 100644 index 0000000..1559ecc --- /dev/null +++ b/src/billing/service/get_user.py @@ -0,0 +1,8 @@ +from core.models import User, Organization + + +def get_actor_from_stripe_customer(stripe_customer_id: str) -> User | Organization | None: + return ( + User.objects.filter(stripe_customer_id=stripe_customer_id).first() + or Organization.objects.filter(stripe_customer_id=stripe_customer_id).first() + ) diff --git a/src/billing/service/plan_change.py b/src/billing/service/plan_change.py new file mode 100644 index 0000000..c731b06 --- /dev/null +++ b/src/billing/service/plan_change.py @@ -0,0 +1,31 @@ +from datetime import datetime +import stripe +from django.db.models import QuerySet +from core.models import User, Organization +from billing.models import UserSubscription, SubscriptionPlan +from billing.service.entitlements import update_user_entitlements +from billing.service.stripe_customer import get_or_create_customer_id + + +def handle_plan_change(user_subscription: UserSubscription, new_plan: SubscriptionPlan) -> UserSubscription: + """ + Handles plan upgrades or downgrades. + """ + # Cancel the current Stripe subscription if necessary + stripe.Subscription.modify( + user_subscription.stripe_subscription_id, + cancel_at_period_end=False, # Cancels immediately + ) + + # Create a new Stripe subscription for the new plan + new_subscription = stripe.Subscription.create( + customer=user_subscription.owner.stripe_customer_id, + items=[{"price": new_plan.stripe_price_id}], + ) + + # Update the UserSubscription object with new plan and subscription id + user_subscription.subscription_plan = new_plan + user_subscription.stripe_subscription_id = new_subscription.id + user_subscription.save() + + return user_subscription diff --git a/src/billing/service/price.py b/src/billing/service/price.py new file mode 100644 index 0000000..009249a --- /dev/null +++ b/src/billing/service/price.py @@ -0,0 +1,8 @@ +import stripe + + +def get_price_id_from_lookup_key(lookup_key: str) -> str: + prices = stripe.Price.list(lookup_keys=[lookup_key]) + if prices.data: + return prices.data[0].id # Assuming the lookup key returns one price + raise ValueError(f"Price with lookup key {lookup_key} not found.") diff --git a/src/billing/service/stripe_customer.py b/src/billing/service/stripe_customer.py new file mode 100644 index 0000000..29a323a --- /dev/null +++ b/src/billing/service/stripe_customer.py @@ -0,0 +1,32 @@ +from core.models import User, Organization +import stripe + + +def get_or_create_customer_id(actor: User | Organization) -> str: + if actor.stripe_customer_id: + return actor.stripe_customer_id + + return create_stripe_customer_id(actor) + + +def create_stripe_customer_id(actor: User | Organization) -> str: + if isinstance(actor, User): + customer = stripe.Customer.create( + email=actor.email, + name=actor.get_full_name(), + ) + actor.stripe_customer_id = customer.id + actor.save() + + return customer.id + + else: + customer = stripe.Customer.create( + email=actor.leader.email, + name=actor.name, + ) + + actor.stripe_customer_id = customer.id + actor.save() + + return customer.id diff --git a/src/billing/service/subscription_ended.py b/src/billing/service/subscription_ended.py new file mode 100644 index 0000000..34dd2f4 --- /dev/null +++ b/src/billing/service/subscription_ended.py @@ -0,0 +1,39 @@ +import stripe + +from core.models import User, Organization +from billing.models import StripeWebhookEvent, UserSubscription + + +def subscription_ended(webhook_event: StripeWebhookEvent) -> None: + event_data: stripe.Subscription = webhook_event.data.object + stripe_customer = event_data.customer + + # Find the user or organization based on the stripe customer + actor = ( + User.objects.filter(stripe_customer_id=stripe_customer).first() + or Organization.objects.filter(stripe_customer_id=stripe_customer).first() + ) + + actor_subscription_plan = None + + if not actor: + # If no user found, try to fetch the subscription plan using the stripe subscription ID + plan = UserSubscription.objects.filter( + stripe_subscription_id=event_data.id, stripe_subscription_id__isnull=False + ).first() # type: ignore[misc] + + if plan: + actor_subscription_plan = plan + actor = plan.owner + else: + print("Error: Could not find user or subscription plan.") + return + + if not actor_subscription_plan: + actor_subscriptions = UserSubscription.filter_by_owner(owner=actor).all() + if not actor_subscriptions: + return + + # Find a subscription plan with the same Stripe subscription ID + if plan_with_same_id := actor_subscriptions.filter(stripe_subscription_id=event_data.id).first(): + plan_with_same_id.end_now() diff --git a/src/billing/service/subscription_handler.py b/src/billing/service/subscription_handler.py new file mode 100644 index 0000000..e0da043 --- /dev/null +++ b/src/billing/service/subscription_handler.py @@ -0,0 +1,86 @@ +import stripe +from typing import Union +from django.utils import timezone + +from core.models import Organization +from billing.models import UserSubscription, SubscriptionPlan +from django.contrib.auth.models import User + +from billing.service.entitlements import update_user_entitlements + + +def create_subscription(owner: Union[User, Organization], subscription_plan: SubscriptionPlan) -> UserSubscription: + """ + Creates a new Stripe subscription for a user or organization. + + Args: + owner: The user or organization subscribing. + subscription_plan: The plan the owner is subscribing to. + + Returns: + A UserSubscription object representing the subscription. + """ + # Create a new Stripe subscription for the given owner (user or organization) + stripe_subscription = stripe.Subscription.create( + customer=owner.stripe_customer_id, + items=[{"price": subscription_plan.stripe_price_id}], + ) + + # Create the corresponding UserSubscription record in your database + user_subscription = UserSubscription.objects.create( + owner=owner, subscription_plan=subscription_plan, stripe_subscription_id=stripe_subscription.id, start_date=timezone.now() + ) + + # Update user entitlements via Stripe entitlements + update_user_entitlements(owner) + + return user_subscription + + +def cancel_subscription(user_subscription: UserSubscription) -> UserSubscription: + """ + Cancels an active Stripe subscription and updates the local subscription record. + + Args: + user_subscription: The subscription to cancel. + + Returns: + The updated UserSubscription object with the end_date set. + """ + stripe.Subscription.delete(subscription=user_subscription.stripe_subscription_id) + + # Mark the subscription as canceled in your local database + user_subscription.end_date = timezone.now() + user_subscription.save() + + # Update entitlements after cancellation + update_user_entitlements(user_subscription.owner) + + return user_subscription + + +def handle_plan_change(user_subscription: UserSubscription, new_plan: SubscriptionPlan) -> UserSubscription: + """ + Updates the user's Stripe subscription to a new plan, with proration handled automatically. + + Args: + user_subscription: The current UserSubscription to update. + new_plan: The new SubscriptionPlan to switch to. + + Returns: + The updated UserSubscription object with the new plan. + """ + stripe.Subscription.modify( + user_subscription.stripe_subscription_id, + cancel_at_period_end=False, # Cancels the current plan immediately + items=[{"price": new_plan.stripe_price_id}], + ) + + # Update the local subscription model + user_subscription.subscription_plan = new_plan + user_subscription.save() + + # Update entitlements after the plan change + update_user_entitlements(user_subscription.owner) + + return user_subscription diff --git a/src/billing/service/test.py b/src/billing/service/test.py new file mode 100644 index 0000000..a316e26 --- /dev/null +++ b/src/billing/service/test.py @@ -0,0 +1,38 @@ +# import os +# +# import stripe +# from django.urls import reverse +# +# from core.models import User +# +# user: User = User.objects.first() +# +# # stripe.billing.MeterEvent.create( +# # event_name="invoices_created", +# # payload={"invoices": "250", "stripe_customer_id": user.stripe_customer_id}, +# # # identifier="id" +# # ) +# # +# # stripe.billing.Meter.list_event_summaries( +# # "" +# # ) +# +# # a = stripe.Customer.create( +# # name=user.get_full_name(), +# # email=user.email +# # ) +# # +# # user.stripe_customer_id = a.id +# # user.save() +# +# # print(a) +# +# # stripe.checkout.Session.create( +# # success_url=os.environ.get("SITE_URL", default="http://127.0.0.1:8000") + reverse("api:public:webhooks:receive_global"), +# # line_items=[ +# # { +# # "price": "price_", +# # "quantity": 1 +# # } +# # ] +# # ) diff --git a/src/billing/signals/__init__.py b/src/billing/signals/__init__.py new file mode 100644 index 0000000..ccb6776 --- /dev/null +++ b/src/billing/signals/__init__.py @@ -0,0 +1 @@ +from . import migrations, usage, stripe diff --git a/src/billing/signals/migrations.py b/src/billing/signals/migrations.py new file mode 100644 index 0000000..0115489 --- /dev/null +++ b/src/billing/signals/migrations.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import logging + +from django.db.models.signals import post_migrate +from django.dispatch import receiver + +from billing.data.default_usage_plans import default_usage_plans, default_subscription_plans, DefaultFeature, DefaultSubscriptionPlan +from billing.models import PlanFeature, PlanFeatureGroup, SubscriptionPlan + + +# +@receiver(post_migrate) +def update_usage_plans(**kwargs): + subscription_plans: dict = {} + + subscription_plan: DefaultSubscriptionPlan + + for subscription_plan in default_subscription_plans: + if plan := SubscriptionPlan.objects.filter(name=subscription_plan.name).first(): + subscription_plans[plan.name] = plan + else: + subscription_plans[subscription_plan.name] = SubscriptionPlan.objects.create( + name=subscription_plan.name, + description=subscription_plan.description, + price_per_month=subscription_plan.price_per_month, + ) + logging.info(f"Added SubscriptionPlan {subscription_plan.name}") + + for group in default_usage_plans: + group_obj, created = PlanFeatureGroup.objects.get_or_create(name=group.name) + + if created: + logging.info(f"Created group {group.name}") + + item: DefaultFeature + for item in group.items: + existing: PlanFeature = PlanFeature.objects.filter( + slug=item.slug, subscription_plan=SubscriptionPlan.objects.get(name=item.subscription_plan.name) + ).first() + + if existing: + description, old_subscription_plan, max_limit_per_month = ( + existing.description, + existing.subscription_plan, + existing.max_limit_per_month, + ) + + existing.description = item.description + existing.max_limit_per_month = item.max_limit_per_month if item.max_limit_per_month != -1 else None + + if ( + existing.description != description + or (existing.max_limit_per_month == None and max_limit_per_month != -1) + or (existing.max_limit_per_month != None and max_limit_per_month == -1) + or (existing.max_limit_per_month != None and existing.max_limit_per_month != max_limit_per_month) + ): + existing.save() + + logging.info(f"Updated PlanFeature description/limits for {item.slug}") + else: + existing = PlanFeature.objects.create( + group=group_obj, + description=item.description, + slug=item.slug, + max_limit_per_month=item.max_limit_per_month, + subscription_plan=SubscriptionPlan.objects.get(name=item.subscription_plan.name), + ) + logging.info(f"Added PlanFeature {item.slug}") diff --git a/src/billing/signals/quotas.py b/src/billing/signals/quotas.py new file mode 100644 index 0000000..d79ba6b --- /dev/null +++ b/src/billing/signals/quotas.py @@ -0,0 +1,15 @@ +# from django.db.models.signals import post_save +# from django.dispatch import receiver +# +# from backend.finance.models import Invoice, Usage +# +# +# @receiver(post_save, sender=Invoice) +# def created_invoice(sender, instance: Invoice, **kwargs): +# Usage.objects.create( +# owner=instance.owner, +# feature="invoices-created", +# quantity=1, +# unit="invocations", +# instance_id=instance.id, +# ) diff --git a/src/billing/signals/stripe/__init__.py b/src/billing/signals/stripe/__init__.py new file mode 100644 index 0000000..ae73ac8 --- /dev/null +++ b/src/billing/signals/stripe/__init__.py @@ -0,0 +1 @@ +from . import webhook_handler diff --git a/src/billing/signals/stripe/webhook_handler.py b/src/billing/signals/stripe/webhook_handler.py new file mode 100644 index 0000000..2b003a7 --- /dev/null +++ b/src/billing/signals/stripe/webhook_handler.py @@ -0,0 +1,19 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from billing.models import StripeWebhookEvent +from billing.service.checkout_completed import checkout_completed +from billing.service.subscription_ended import subscription_ended +from billing.service.entitlements import entitlements_updated_via_stripe_webhook + + +@receiver(post_save, sender=StripeWebhookEvent) +def stripe_webhook_event_created(sender, instance: StripeWebhookEvent, **kwargs): + match instance.event_type: + case "checkout.session.completed": + checkout_completed(instance) + case "customer.subscription.deleted": + subscription_ended(instance) + case "entitlements.active_entitlement_summary.updated": + entitlements_updated_via_stripe_webhook(instance) + case _: + print(f"Unhandled event type: {instance.event_type}") diff --git a/src/billing/signals/usage.py b/src/billing/signals/usage.py new file mode 100644 index 0000000..ac3da20 --- /dev/null +++ b/src/billing/signals/usage.py @@ -0,0 +1,41 @@ +import logging +from datetime import datetime + +import stripe +from django.db.models.signals import post_save +from django.dispatch import receiver + +from core.models import User +from billing.models import BillingUsage + +logger = logging.getLogger(__name__) + + +@receiver(post_save, sender=BillingUsage) +def usage_occurred(sender, instance: BillingUsage, created, **kwargs): + if not created or instance.processed: + return + + if instance.event_type != "usage": + return # may add storage at a later point + + if not instance.owner: + print("CANNOT HANDLE ORGS AT THE MOMENT!") + return # todo: cannot handle organisations at the moment + + stripe_customer_id = instance.owner.stripe_customer_id + + if not stripe_customer_id: + print(f"No stripe customer id for actor #{'usr_' if isinstance(instance.owner, User) else 'org_'}{instance.owner.id}") + return # todo + + meter_event = stripe.billing.MeterEvent.create( + event_name=instance.event_name, payload={"value": str(instance.quantity), f"stripe_customer_id": stripe_customer_id} + ) + + if meter_event.created: + instance.stripe_unique_usage_identifier = meter_event.identifier + instance.set_processed(datetime.fromtimestamp(meter_event.created)) + + return + diff --git a/src/billing/templates/pages/billing/dashboard/all_subscriptions.html b/src/billing/templates/pages/billing/dashboard/all_subscriptions.html new file mode 100644 index 0000000..4353101 --- /dev/null +++ b/src/billing/templates/pages/billing/dashboard/all_subscriptions.html @@ -0,0 +1,71 @@ +
+
+
+ All Subscriptions + +
+ + + + + + + + + + + + + {% for subscription in all_user_subscriptions|dictsortreversed:"start_date" %} + + + + + + {% if subscription.has_ended %} + + {% else %} + + + {% endif %} + + {% endfor %} + +
StatusPlan NamePriceStartedEndedActions
+ {% if subscription.has_ended %} + Ended + {% else %} + Ongoing + {% endif %} + {{ subscription.subscription_plan.name }} + {% if subscription.subscription_plan.price_per_month == -1 %} + {{ subscription.custom_subscription_price_per_month }} + {% else %} + {{ subscription.subscription_plan.price_per_month }} + {% endif %} + {{ subscription.start_date }}{{ subscription.end_date | default_if_none:"" }} + + + +
+
+
\ No newline at end of file diff --git a/src/billing/templates/pages/billing/dashboard/choose_plan_section.html b/src/billing/templates/pages/billing/dashboard/choose_plan_section.html new file mode 100644 index 0000000..b6994e4 --- /dev/null +++ b/src/billing/templates/pages/billing/dashboard/choose_plan_section.html @@ -0,0 +1,199 @@ +
+
+
+ Current Subscription Plan: {{ active_subscription.subscription_plan.name | default_if_none:"Not subscribed" }} +
+ +
+
+ +
+
+

Starter

+

+ For individual freelancers or small businesses with less recurring customers +

+
+ ÂŖ5 + /month +
+
    +
  • + + Unlimited Customers +
  • +
  • + + + Unlimited Invoices (first 10 on us!)* + +
  • +
  • + + Basic Customer Onboarding +
  • +
  • + + Emailing (automated, normal & bulk) +
  • +
  • + + File Storage (10 GB included!) +
  • +
  • + + + No Limits * + +
  • +
+ {% if active_subscription and active_subscription.subscription_plan.name|lower == "starter" %} + + Your active plan + + {% else %} + + {% endif %} +
+
+

Growth

+

+ For small-medium sized businesses that require more features or have a wider customer base +

+
+ ÂŖ15 + /month +
+
    +
  • + Everything in Starter, plus: +
  • +
  • + + Automated Invoice Reminders +
  • +
  • + + Advanced Customer Onboarding +
  • +
  • + + API Access +
  • +
  • + + File Storage (50 GB included!) +
  • +
  • + + + No Limits * + +
  • +
+ {% if active_subscription and active_subscription.subscription_plan.name|lower == "growth" %} + + Your active plan + + {% else %} + + {% endif %} +
+
+

Enterprise

+

+ For large businesses or if your business needs a bit more + customisation +

+
+ + Custom + +
+
    +
  • + + Choose a price that fits your business +
  • +
  • + + + Organization Mode + + + + +
  • +
  • + + Dedicated Customer Success +
  • +
  • + + SLA can be discussed +
  • +
  • + + + Analytics and live updates + + + + +
  • +
  • + + Pre-release notifications +
  • +
+ + Contact Us + +
+
+ +
+ + {% include "pages/billing/dashboard/starter_usages.html" %} + + + {% include "pages/billing/dashboard/growth_usages.html" %} +
+
+
\ No newline at end of file diff --git a/src/billing/templates/pages/billing/dashboard/dashboard.html b/src/billing/templates/pages/billing/dashboard/dashboard.html new file mode 100644 index 0000000..d8f6310 --- /dev/null +++ b/src/billing/templates/pages/billing/dashboard/dashboard.html @@ -0,0 +1,15 @@ +{% extends base|default:"base/base.html" %} +{% load listfilters %} +{% block content %} + {% load get_first_n_items from listfilters %} +
+
+
+

Billing

+ +
+
+ {% include "pages/billing/dashboard/choose_plan_section.html" %} + {% include "pages/billing/dashboard/all_subscriptions.html" %} +{% endblock %} diff --git a/src/billing/templates/pages/billing/dashboard/growth_usages.html b/src/billing/templates/pages/billing/dashboard/growth_usages.html new file mode 100644 index 0000000..c0e71d2 --- /dev/null +++ b/src/billing/templates/pages/billing/dashboard/growth_usages.html @@ -0,0 +1,137 @@ +
+
+
+ Growth Plan - Usage-Based Pricing +
+ +
+ + +
+ +
+ Invoices +
+
+ + + + + + + + + + + + + + + + + + + + + +
QuantityUnit Price
First 80 invoicesIncluded for free
Next 170 invoicesÂŖ0.04/invoice
Greater than 250 invoicesÂŖ0.03/invoice
+
+
+ + +
+ +
+ Recurring Invoices +
+
+ + + + + + + + + + + + + + + + + + + + + +
QuantityUnit Price
First 100 invocationsIncluded for free
Next 400 invocationsÂŖ0.07/invocation
Greater than 500 invocationsÂŖ0.04/invocation
+
+
+ + +
+ +
+ Invoice Reminders +
+
+ + + + + + + + + + + + + + + + + + + + + +
QuantityUnit Price
First 400 invocationsIncluded for free
Next 600 invocationsÂŖ0.02/invocation
Greater than 1000 invocationsÂŖ0.01/invocation
+
+
+ + +
+ +
+ Advanced Onboarding +
+
+ + + + + + + + + + + + + + + + + + + + + +
QuantityUnit Price
First 5 customersIncluded for free
Next 20 customersÂŖ0.10/customer
Greater than 25 customersÂŖ0.08/customer
+
+
+
+
diff --git a/src/billing/templates/pages/billing/dashboard/starter_usages.html b/src/billing/templates/pages/billing/dashboard/starter_usages.html new file mode 100644 index 0000000..0ae8d73 --- /dev/null +++ b/src/billing/templates/pages/billing/dashboard/starter_usages.html @@ -0,0 +1,73 @@ +
+
+
+ Starter Plan - Usage-Based Pricing +
+ +
+ + +
+ +
+ Invoices +
+
+ + + + + + + + + + + + + + + + + + + + + +
QuantityUnit Price
First 10 invoicesIncluded for free
Next 40 invoicesÂŖ0.05/invoice
Greater than 50 invoicesÂŖ0.035/invoice
+
+
+ + +
+ +
+ Recurring Invoices +
+
+ + + + + + + + + + + + + + + + + + + + + +
QuantityUnit Price
First 10 invocationsIncluded for free
Next 20 invocationsÂŖ0.07/invocation
Greater than 30 invocationsÂŖ0.05/invocation
+
+
+
+
diff --git a/src/billing/urls.py b/src/billing/urls.py new file mode 100644 index 0000000..7fd3078 --- /dev/null +++ b/src/billing/urls.py @@ -0,0 +1,21 @@ +from django.urls import path + +from billing.views.dashboard import billing_dashboard_endpoint, all_subscriptions_htmx_endpoint +from billing.views.stripe_misc import customer_client_portal_endpoint +from billing.views.stripe_webhooks import stripe_listener_webhook_endpoint + +from .views.return_urls.success import stripe_success_return_endpoint +from .views.change_plan import change_plan_endpoint + +urlpatterns = [ + path("dashboard/billing/", billing_dashboard_endpoint, name="dashboard"), + path("api/billing/all_subscriptions/", all_subscriptions_htmx_endpoint, name="all_subscriptions"), + # path("dashboard/billing/", RedirectView.as_view(url="/dashboard"), name="dashboard"), + path("api/public/webhooks/receive/payments/stripe/", stripe_listener_webhook_endpoint, name="receive_stripe_webhook"), + path("api/billing/stripe/change_plan/", change_plan_endpoint, name="change_plan"), + path("dashboard/billing/stripe/portal/", customer_client_portal_endpoint, name="stripe_customer_portal"), + path("dashboard/billing/stripe/checkout/response/success/", stripe_success_return_endpoint, name="stripe_checkout_success_response"), + path("dashboard/billing/stripe/checkout/response/failed/", stripe_success_return_endpoint, name="stripe_checkout_failed_response"), +] + +app_name = "billing" diff --git a/src/billing/views.py b/src/billing/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/src/billing/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/src/billing/views/__init__.py b/src/billing/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/billing/views/change_plan.py b/src/billing/views/change_plan.py new file mode 100644 index 0000000..e849c31 --- /dev/null +++ b/src/billing/views/change_plan.py @@ -0,0 +1,111 @@ +import logging + +import stripe +from django.contrib import messages +from django.db.models import QuerySet +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse + +from core.decorators import htmx_only, web_require_scopes +from core.models import User +from core.types.requests import WebRequest +from billing.models import SubscriptionPlan, UserSubscription, StripeCheckoutSession +from billing.service.stripe_customer import get_or_create_customer_id + +logger = logging.getLogger(__name__) + + +@web_require_scopes("billing:manage", api=True, htmx=True) +@htmx_only("billing:dashboard") +def change_plan_endpoint(request: WebRequest): + + plan: SubscriptionPlan | None = None + + if plan_filter := request.POST.get("plan_name"): + plan = SubscriptionPlan.objects.filter(name=plan_filter).first() + elif plan_filter := request.POST.get("plan_id"): + plan = SubscriptionPlan.objects.filter(id=plan_filter).first() + + if not plan: + messages.error(request, "Invalid plan") + return render(request, "base/toast.html") + elif plan.price_per_month == -1 or plan.name.lower() == "enterprise": + print("THIS PLAN IS ENTERPRISE, currently not implemented") + messages.error(request, "Invalid plan (not yet implemented)") + return render(request, "base/toast.html") + + users_plans: QuerySet[UserSubscription] = UserSubscription.filter_by_owner(request.actor) + + if plan.price_per_month == 0 and users_plans.exists(): + messages.error( + request, + """ + Unfortunately you have already used up your free trial. Please upgrade to a paid plan to continue. + If you have another query, feel free to book a call with the project lead + founder! + + Book here + + """, + ) + return render(request, "base/toast.html", {"autohide": False}) + + users_active_plans: QuerySet[UserSubscription] = users_plans.filter(end_date__isnull=True) + # if users_active_plans.exists(): + # for active_plan in users_active_plans: + # active_plan.end_date = timezone_now() + # active_plan.save() + + line_items = [{"adjustable_quantity": {"enabled": False}, "quantity": 1, "price": plan.stripe_price_id}] # type: ignore + + checkout_session_django_object = ( + StripeCheckoutSession.objects.create(user=request.actor, plan=plan) + if isinstance(request.actor, User) + else StripeCheckoutSession.objects.create(organization=request.actor, plan=plan) + ) + + for feature in plan.features.all(): + if not feature.stripe_price_id: + continue + + checkout_session_django_object.features.add(feature) + + line_items.append( + { + # "adjustable_quantity": { + # "enabled": False + # }, + "price": feature.stripe_price_id, + # "quantity": 1, + } + ) + + customer_id = get_or_create_customer_id(request.actor) + + if isinstance(request.actor, User): + customer_email = request.actor.email + else: + customer_email = request.actor.leader.email + + checkout_session = stripe.checkout.Session.create( + customer=customer_id, + customer_email=customer_email if not customer_email else None, # type: ignore[arg-type] + line_items=line_items, # type: ignore[arg-type] + mode="subscription", + # return_url="http://127.0.0.1:8000" + reverse("billing:stripe_checkout_failed_response"), + cancel_url=request.build_absolute_uri(reverse("billing:dashboard")), + success_url=request.build_absolute_uri(reverse("billing:stripe_checkout_success_response")), + metadata={"dj_checkout_uuid": checkout_session_django_object.uuid, "dj_subscription_plan_id": str(plan.id)}, + saved_payment_method_options={"payment_method_save": "enabled"}, + ) + + checkout_session_django_object.stripe_session_id = checkout_session.id + + checkout_session_django_object.save() + + # UserSubscription.objects.create(owner=request.actor, subscription_plan=plan) + messages.success(request, "Great! Redirecting you to stripe now!") + r = HttpResponse(status=200) + r["HX-Redirect"] = str(checkout_session.url) + return r diff --git a/src/billing/views/dashboard.py b/src/billing/views/dashboard.py new file mode 100644 index 0000000..d9cc96e --- /dev/null +++ b/src/billing/views/dashboard.py @@ -0,0 +1,50 @@ +from django.shortcuts import render + +from core.decorators import web_require_scopes +from billing.models import UserSubscription, SubscriptionPlan +from core.types.requests import WebRequest + + +@web_require_scopes("billing:manage", api=True, htmx=True) +def billing_dashboard_endpoint(request: WebRequest): + context: dict = {} + + subscriptions = UserSubscription.filter_by_owner(request.actor).select_related("subscription_plan").all() + all_subscription_plans = SubscriptionPlan.objects.all() + + if subscriptions.exists(): + context["free_plan_available"] = True + + context.update( + { + "active_subscription": subscriptions.filter(end_date__isnull=True).first(), + "all_user_subscriptions": subscriptions, + "all_subscription_plans": all_subscription_plans, + } + ) + + return render( + request, + "pages/billing/dashboard/dashboard.html", + context, + ) + + +@web_require_scopes("billing:manage", api=True, htmx=True) +def all_subscriptions_htmx_endpoint(request: WebRequest): + context: dict = {} + + subscriptions = UserSubscription.filter_by_owner(request.actor).select_related("subscription_plan").all() + + context.update( + { + "active_subscription": subscriptions.filter(end_date__isnull=True).first(), + "all_user_subscriptions": subscriptions, + } + ) + + return render( + request, + "pages/billing/dashboard/all_subscriptions.html", + context, + ) diff --git a/src/billing/views/return_urls/failed.py b/src/billing/views/return_urls/failed.py new file mode 100644 index 0000000..66e7f5d --- /dev/null +++ b/src/billing/views/return_urls/failed.py @@ -0,0 +1,9 @@ +from django.contrib import messages +from django.shortcuts import redirect + +from core.types.requests import WebRequest + + +def stripe_failed_return_endpoint(request: WebRequest): + messages.warning(request, "FAILED RESPONSE") + return redirect("billing:dashboard") diff --git a/src/billing/views/return_urls/success.py b/src/billing/views/return_urls/success.py new file mode 100644 index 0000000..f35889c --- /dev/null +++ b/src/billing/views/return_urls/success.py @@ -0,0 +1,7 @@ +from django.shortcuts import redirect + +from core.types.requests import WebRequest + + +def stripe_success_return_endpoint(request: WebRequest): + return redirect("billing:dashboard") diff --git a/src/billing/views/stripe_misc.py b/src/billing/views/stripe_misc.py new file mode 100644 index 0000000..ced186d --- /dev/null +++ b/src/billing/views/stripe_misc.py @@ -0,0 +1,30 @@ +import stripe +from django.http import HttpResponseRedirect, HttpResponse +from django.urls import reverse, resolve, NoReverseMatch + +from core.decorators import web_require_scopes +from core.types.requests import WebRequest + +from billing.service.stripe_customer import get_or_create_customer_id + + +@web_require_scopes("billing:manage", api=True, htmx=True) +def customer_client_portal_endpoint(request: WebRequest): + if NEXT := request.GET.get("back"): + try: + resolve(NEXT) + except NoReverseMatch: + NEXT = None + + customer_id = get_or_create_customer_id(request.actor) + + stripe_resp = stripe.billing_portal.Session.create( + customer=customer_id, return_url=request.build_absolute_uri(NEXT or reverse("dashboard")) + ) + + if request.htmx: + response = HttpResponse(status=200) + response["HX-Redirect"] = stripe_resp.url + return response + + return HttpResponseRedirect(stripe_resp.url) diff --git a/src/billing/views/stripe_webhooks.py b/src/billing/views/stripe_webhooks.py new file mode 100644 index 0000000..cb05823 --- /dev/null +++ b/src/billing/views/stripe_webhooks.py @@ -0,0 +1,33 @@ +import stripe +from rest_framework.permissions import AllowAny +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.decorators import api_view, authentication_classes, permission_classes +from billing.billing_settings import STRIPE_WEBHOOK_ENDPOINT_SECRET +from billing.models import StripeWebhookEvent +from django.views.decorators.csrf import csrf_exempt + + +@api_view(["POST"]) +@authentication_classes([]) # No auth required for webhooks +@permission_classes([AllowAny]) +@csrf_exempt +def stripe_listener_webhook_endpoint(request: Request): + payload = request.body + sig_header = request.META.get("HTTP_STRIPE_SIGNATURE", "") + + try: + event = stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_ENDPOINT_SECRET) + print(f"Webhook received: {event['type']}") + except ValueError as error: + print(f"Invalid payload: {error}") + return Response(status=400) + except stripe.error.SignatureVerificationError as error: + print(f"Invalid signature: {error}") + return Response(status=400) + + # Store event in database + StripeWebhookEvent.objects.create(event_id=event.id, event_type=event["type"], data=event["data"], raw_event=event) + + # Call specific event handler (signal) + return Response(status=200) diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/admin.py b/src/core/admin.py new file mode 100644 index 0000000..d2afee1 --- /dev/null +++ b/src/core/admin.py @@ -0,0 +1,76 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + +from core.api.public import APIAuthToken + +from core.models import ( + PasswordSecret, + AuditLog, + LoginLog, + Error, + TracebackError, + UserSettings, + Notification, + Organization, + TeamInvitation, + TeamMemberPermission, + User, + FeatureFlags, + VerificationCodes, + EmailSendStatus, +) + +from django.conf import settings + +# from django.contrib.auth.models imp/ort User +# admin.register(Invoice) +admin.site.register( + [ + UserSettings, + PasswordSecret, + AuditLog, + LoginLog, + Error, + TracebackError, + Notification, + Organization, + TeamInvitation, + TeamMemberPermission, + FeatureFlags, + VerificationCodes, + APIAuthToken, + ] +) + +if getattr(settings, "BILLING_ENABLED", False): + from billing.models import PlanFeature, PlanFeatureGroup, SubscriptionPlan, UserSubscription + + admin.site.register([PlanFeature, PlanFeatureGroup, SubscriptionPlan, UserSubscription]) + +class EmailSendStatusAdmin(admin.ModelAdmin): + readonly_fields = ["aws_message_id"] + +admin.site.register(EmailSendStatus, EmailSendStatusAdmin) + +# admin.site.unregister(User) +fields = list(UserAdmin.fieldsets) # type: ignore[arg-type] +fields[0] = ( + None, + { + "fields": ( + "username", + "password", + "logged_in_as_team", + "awaiting_email_verification", + "stripe_customer_id", + "entitlements", + "require_change_password", + ) + }, +) +UserAdmin.fieldsets = tuple(fields) +admin.site.register(User, UserAdmin) + +admin.site.site_header = "Strelix Core Admin" +admin.site.index_title = "Strelix" +admin.site.site_title = "Strelix | Administration" diff --git a/src/core/api/__init__.py b/src/core/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/base/__init__.py b/src/core/api/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/base/breadcrumbs.py b/src/core/api/base/breadcrumbs.py new file mode 100644 index 0000000..765bd90 --- /dev/null +++ b/src/core/api/base/breadcrumbs.py @@ -0,0 +1,19 @@ +from django.shortcuts import render + +from core.types.requests import WebRequest +from core.service.base.breadcrumbs import get_breadcrumbs + + +def update_breadcrumbs_endpoint(request: WebRequest): + url = request.GET.get("url") + + breadcrumb_dict: dict = get_breadcrumbs(url=url) + return render( + request, + "core/base/breadcrumbs.html", + { + "breadcrumb": breadcrumb_dict.get("breadcrumb"), + "swapping": True, + # "swap": True + }, + ) diff --git a/src/core/api/base/notifications.py b/src/core/api/base/notifications.py new file mode 100644 index 0000000..2b4c0a1 --- /dev/null +++ b/src/core/api/base/notifications.py @@ -0,0 +1,41 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render + +from core.models import Notification +from core.types.htmx import HtmxHttpRequest + + +def get_notification_html(request: HtmxHttpRequest): + user_notifications = Notification.objects.filter(user=request.user).order_by("-date") + count = user_notifications.count() + + if count > 5: + user_notifications = user_notifications[:5] + + return render( + request, + "core/base/topbar/_notification_dropdown_items.html", + {"notifications": user_notifications, "notif_count": count}, + ) + + +def get_notification_count_html(request: HtmxHttpRequest): + user_notifications = Notification.objects.filter(user=request.user).count() + return HttpResponse(f"{user_notifications}") + + +def delete_notification(request: HtmxHttpRequest, id: int): + notif = Notification.objects.filter(id=id, user=request.user).first() + + if notif is None or notif.user != request.user: + if request.htmx: + messages.error(request, "Notification not found") + return render(request, "base/toasts.html") + return HttpResponse(status=404, content="Notification not found") + + notif.delete() + + response = HttpResponse(status=200) + response["HX-Trigger"] = "refresh_notification_count" + return response diff --git a/src/core/api/base/urls.py b/src/core/api/base/urls.py new file mode 100644 index 0000000..f9b4d80 --- /dev/null +++ b/src/core/api/base/urls.py @@ -0,0 +1,27 @@ +from django.urls import path +from . import notifications, breadcrumbs +from ...views.modals.open import open_modal_endpoint + +# from . import modal + +urlpatterns = [ + path( + "modals//retrieve", + open_modal_endpoint, + name="modal retrieve", + ), + path( + "notifications/get", + notifications.get_notification_html, + name="notifications get", + ), + path("notifications/get_count", notifications.get_notification_count_html, name="notifications get count"), + path( + "notifications/delete/", + notifications.delete_notification, + name="notifications delete", + ), + path("breadcrumbs/refetch/", breadcrumbs.update_breadcrumbs_endpoint, name="breadcrumbs refetch"), +] + +app_name = "base" diff --git a/src/core/api/emails/__init__.py b/src/core/api/emails/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/emails/fetch.py b/src/core/api/emails/fetch.py new file mode 100644 index 0000000..eff9d2d --- /dev/null +++ b/src/core/api/emails/fetch.py @@ -0,0 +1,43 @@ +from django.core.paginator import Paginator, Page +from django.db.models import Q, QuerySet +from django.http import HttpResponse +from django.shortcuts import render, redirect +from django_ratelimit.core import is_ratelimited + +from core.decorators import web_require_scopes +from core.models import EmailSendStatus +from core.types.htmx import HtmxHttpRequest + + +@web_require_scopes("emails:read", True, True) +def fetch_all_emails(request: HtmxHttpRequest): + if is_ratelimited(request, group="fetch_all_emails", key="user", rate="2/4s", increment=True) or is_ratelimited( + request, + group="fetch_all_emails", + key="user", + rate="5/10s", + increment=True or is_ratelimited(request, group="fetch_all_emails", key="user", rate="20/2m", increment=True), + ): + return HttpResponse(status=429) + context = {} + if not request.htmx: + return redirect("quotas") + + search_text = request.GET.get("search") + page_num = request.GET.get("page") + + if request.user.logged_in_as_team: + results: QuerySet[EmailSendStatus] = EmailSendStatus.objects.filter(organization=request.user.logged_in_as_team) + else: + results = EmailSendStatus.objects.filter(user=request.user) + + if search_text: + results = results.filter(Q(recipient__icontains=search_text)) + + result: Page | QuerySet = results.order_by("-id") + + paginator = Paginator(result, 8) + result = paginator.get_page(page_num) + + context.update({"emails": result}) + return render(request, "pages/emails/_fetch_body.html", context) diff --git a/src/core/api/emails/status.py b/src/core/api/emails/status.py new file mode 100644 index 0000000..d9b5529 --- /dev/null +++ b/src/core/api/emails/status.py @@ -0,0 +1,99 @@ +from logging import exception +from typing import TypedDict + +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.views.decorators.http import require_POST +from django_ratelimit.core import is_ratelimited +from mypy_boto3_sesv2.type_defs import GetMessageInsightsResponseTypeDef, InsightsEventTypeDef + +from core.decorators import htmx_only, feature_flag_check, web_require_scopes +from core.utils.settings_helper import EMAIL_CLIENT +from core.models import EmailSendStatus +from core.types.htmx import HtmxHttpRequest + +@require_POST +@htmx_only("emails:dashboard") +@feature_flag_check("areUserEmailsAllowed", status=True, api=True, htmx=True) +@web_require_scopes("emails:read", True, True) +def get_status_view(request: HtmxHttpRequest, status_id: str) -> HttpResponse: + try: + if request.user.logged_in_as_team: + EMAIL_STATUS = EmailSendStatus.objects.get(organization=request.user.logged_in_as_team, id=status_id) + else: + EMAIL_STATUS = EmailSendStatus.objects.get(user=request.user, id=status_id) + except EmailSendStatus.DoesNotExist: + messages.error(request, "Status not found") + return render(request, "base/toast.html") + + message_insight = get_message_insights(message_id=EMAIL_STATUS.aws_message_id) # type: ignore[arg-type] + + if isinstance(message_insight, str): + messages.error(request, message_insight) + return render(request, "base/toast.html", {"autohide": False}) + + important_info = get_important_info_from_response(message_insight) + + EMAIL_STATUS.status = important_info["status"] + EMAIL_STATUS.updated_status_at = important_info["most_recent_event"]["Timestamp"] + EMAIL_STATUS.save() + + messages.success(request, f"Status updated to {important_info['status']}") + return render(request, "base/toast.html", {"autohide": False}) + + +@require_POST +@htmx_only("emails:dashboard") +@feature_flag_check("areUserEmailsAllowed", status=True, api=True, htmx=True) +def refresh_all_statuses_view(request: HtmxHttpRequest) -> HttpResponse: + if is_ratelimited(request, group="email-refresh_all_statuses", key="user", rate="5/10m", increment=True) or is_ratelimited( + request, group="email-refresh_all_statuses", key="user", rate="1/m", increment=True + ): + messages.error(request, "Woah, slow down! Refreshing the statuses takes a while, give us a break!") + return render(request, "base/toast.html") + if request.user.logged_in_as_team: + ALL_STATUSES = EmailSendStatus.objects.filter(organization=request.user.logged_in_as_team) + else: + ALL_STATUSES = EmailSendStatus.objects.filter(user=request.user) + + for status in ALL_STATUSES: + response = get_message_insights(message_id=status.aws_message_id) # type: ignore[arg-type] + + if isinstance(response, str): + messages.error(request, response) + continue + + important_info = get_important_info_from_response(response) + + status.status = important_info["status"] + status.updated_status_at = important_info["most_recent_event"]["Timestamp"] + + ALL_STATUSES.bulk_update(ALL_STATUSES, fields=["status", "updated_status_at", "updated_at"]) + + messages.success(request, "All statuses have been refreshed") + http_response = HttpResponse(status=200) + http_response["HX-Refresh"] = "true" + return http_response + + +class ImportantInfo(TypedDict): + most_recent_event: InsightsEventTypeDef + status: str + + +def get_important_info_from_response(response: GetMessageInsightsResponseTypeDef) -> ImportantInfo: + return {"most_recent_event": (most_recent_event := response["Insights"][0]["Events"][0]), "status": most_recent_event["Type"].lower()} + + +def get_message_insights(message_id: str) -> GetMessageInsightsResponseTypeDef | str: + try: + response = EMAIL_CLIENT.get_message_insights(MessageId=message_id) + return response + except EMAIL_CLIENT.exceptions.NotFoundException: + return "A message was not found with this ID. Maybe wait for it to process" + except EMAIL_CLIENT.exceptions.BadRequestException: + return "Something went wrong when trying to fetch the email with this ID" + except Exception as err: + exception(err) + return "Something went wrong when trying to fetch the email with this ID" diff --git a/src/core/api/emails/urls.py b/src/core/api/emails/urls.py new file mode 100644 index 0000000..2b0f88d --- /dev/null +++ b/src/core/api/emails/urls.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from django.urls import path + +from . import fetch, status +# from . import send + +urlpatterns = [ + # path( + # "send/single/", + # send.send_single_email_view, + # name="send single", + # ), + # path( + # "send/bulk/", + # send.send_bulk_email_view, + # name="send bulk", + # ), + path("fetch/", fetch.fetch_all_emails, name="fetch"), + path("get_status//", status.get_status_view, name="get_status"), + path("refresh_statuses/", status.refresh_all_statuses_view, name="refresh statuses"), +] + +app_name = "emails" diff --git a/src/core/api/healthcheck/__init__.py b/src/core/api/healthcheck/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/healthcheck/healthcheck.py b/src/core/api/healthcheck/healthcheck.py new file mode 100644 index 0000000..4c1862d --- /dev/null +++ b/src/core/api/healthcheck/healthcheck.py @@ -0,0 +1,17 @@ +from django.db import connection, OperationalError +from django.http import HttpRequest, HttpResponse +from login_required import login_not_required + + +@login_not_required +def ping(request: HttpRequest) -> HttpResponse: + return HttpResponse("pong") + + +@login_not_required +def healthcheck(request: HttpRequest) -> HttpResponse: + try: + connection.ensure_connection() + return HttpResponse(status=200, content="All operations are up and running!") + except OperationalError: + return HttpResponse(status=503, content="Service Unavailable") diff --git a/src/core/api/healthcheck/urls.py b/src/core/api/healthcheck/urls.py new file mode 100644 index 0000000..7d27122 --- /dev/null +++ b/src/core/api/healthcheck/urls.py @@ -0,0 +1,18 @@ +from django.urls import path + +from . import healthcheck + +urlpatterns = [ + path( + "ping/", + healthcheck.ping, + name="ping", + ), + path( + "healthcheck/", + healthcheck.healthcheck, + name="healthcheck", + ), +] + +app_name = "healthcheck" diff --git a/src/core/api/landing_page/__init__.py b/src/core/api/landing_page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/landing_page/email_waitlist.py b/src/core/api/landing_page/email_waitlist.py new file mode 100644 index 0000000..1733876 --- /dev/null +++ b/src/core/api/landing_page/email_waitlist.py @@ -0,0 +1,49 @@ +from textwrap import dedent + +from login_required import login_not_required + +from core.service.boto3.handler import BOTO3_HANDLER +from core.types.requests import WebRequest + +from django.http import HttpResponse + +from core.utils.settings_helper import send_email + + +@login_not_required +def join_waitlist_endpoint(request: WebRequest): + email_address = request.POST.get("email", "") + name = request.POST.get("name", "") + + if not email_address: + return HttpResponse(status=400) + + if not BOTO3_HANDLER.initiated: + return HttpResponse(status=500) + + BOTO3_HANDLER.dynamodb_client.put_item(TableName="myfinances-emails", Item={"email": {"S": email_address}, "name": {"S": name}}) + + content = """ +
+ Successfully registered! Expect some discounts and updates as we progress in our journey :) +
+ """ + + send_email( + destination=email_address, + subject="Welcome aboard", + content=dedent( + f""" + Thank you for joining our waitlist! + + We're excited to have you on board and will be in touch with more updates as we progress in our journey. + + Stay tuned for discounts, updates and personal direct emails from our founder! + + Best regards, + The MyFinances Team + """ + ).strip(), + ) + + return HttpResponse(status=200, content=dedent(content).strip()) diff --git a/src/core/api/landing_page/urls.py b/src/core/api/landing_page/urls.py new file mode 100644 index 0000000..309fdf7 --- /dev/null +++ b/src/core/api/landing_page/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import email_waitlist + +urlpatterns = [ + path("join_waitlist/", email_waitlist.join_waitlist_endpoint, name="join_waitlist"), +] + +app_name = "landing_page" diff --git a/src/core/api/maintenance/__init__.py b/src/core/api/maintenance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/maintenance/now.py b/src/core/api/maintenance/now.py new file mode 100644 index 0000000..c2c1eee --- /dev/null +++ b/src/core/api/maintenance/now.py @@ -0,0 +1,31 @@ +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +from login_required import login_not_required + +from core.service.maintenance.tasks import execute_maintenance_tasks, get_maintenance_tasks +from core.service.webhooks.auth import authenticate_api_key + +from core.service.maintenance.expire.run import expire_and_cleanup_objects + +import logging + +from core.types.requests import WebRequest + +logger = logging.getLogger(__name__) + + +@require_POST +@csrf_exempt +@login_not_required +def handle_maintenance_now_endpoint(request: WebRequest): + logger.info("Received routine cleanup handler. Now authenticating...") + api_auth_response = authenticate_api_key(request) + + if api_auth_response.failed: + logger.info(f"Maintenance auth failed: {api_auth_response.error}") + return JsonResponse({"message": api_auth_response.error, "success": False}, status=api_auth_response.status_code or 400) + + output_str = execute_maintenance_tasks(get_maintenance_tasks()) + logger.info(output_str) + return JsonResponse({"message": output_str, "success": True}, status=200) diff --git a/src/core/api/maintenance/urls.py b/src/core/api/maintenance/urls.py new file mode 100644 index 0000000..cec9c6d --- /dev/null +++ b/src/core/api/maintenance/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from core.api.maintenance.now import handle_maintenance_now_endpoint + +urlpatterns = [ + path("cleanup/", handle_maintenance_now_endpoint, name="cleanup"), +] + +app_name = "maintenance" diff --git a/src/core/api/public/__init__.py b/src/core/api/public/__init__.py new file mode 100644 index 0000000..496d780 --- /dev/null +++ b/src/core/api/public/__init__.py @@ -0,0 +1 @@ +from .models import APIAuthToken diff --git a/src/core/api/public/authentication.py b/src/core/api/public/authentication.py new file mode 100644 index 0000000..2b2135f --- /dev/null +++ b/src/core/api/public/authentication.py @@ -0,0 +1,66 @@ +from typing import Type + +from rest_framework.authentication import TokenAuthentication, get_authorization_header +from rest_framework.exceptions import AuthenticationFailed +from django.utils.translation import gettext_lazy as _ +from core.api.public.models import APIAuthToken +from core.models import User, Organization + +from rest_framework import exceptions + + +class CustomBearerAuthentication(TokenAuthentication): + keyword = "Bearer" + + def get_model(self) -> Type[APIAuthToken]: + return APIAuthToken + + def authenticate(self, request): + auth = get_authorization_header(request).split() + + if not auth or auth[0].lower() != self.keyword.lower().encode(): + return None + + if len(auth) == 1: + msg = _("Invalid token header. No credentials provided.") + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _("Invalid token header. Token string should not contain spaces.") + raise exceptions.AuthenticationFailed(msg) + + try: + token = auth[1].decode() + except UnicodeError: + msg = _("Invalid token header. Token string should not contain invalid characters.") + raise exceptions.AuthenticationFailed(msg) + + user_or_org, token = self.authenticate_credentials(token) + + request.actor = user_or_org + + if isinstance(user_or_org, Organization): + request.team = user_or_org + request.team_id = user_or_org.id + else: + request.team = None + request.team_id = None + + return (user_or_org, token) + + def authenticate_credentials(self, raw_key) -> tuple[User | Organization | None, APIAuthToken]: + model = self.get_model() + + try: + token = model.objects.get(hashed_key=model.hash_raw_key(raw_key), active=True) + except model.DoesNotExist: + raise AuthenticationFailed(_("Invalid token.")) + + if token.has_expired: + raise AuthenticationFailed(_("Token has expired.")) + + user_or_org = token.user or token.organization + + if user_or_org is None: + raise AuthenticationFailed(_("Associated user or organization not found.")) + + return user_or_org, token diff --git a/src/core/api/public/decorators.py b/src/core/api/public/decorators.py new file mode 100644 index 0000000..785f5c4 --- /dev/null +++ b/src/core/api/public/decorators.py @@ -0,0 +1,44 @@ +from functools import wraps +from rest_framework import status + +from core.models import TeamMemberPermission +from core.api.public.helpers.response import APIResponse + +import logging + +logger = logging.getLogger(__name__) + + +def require_scopes(scopes): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + token = request.auth + if not token: + logger.info( + f"Authentication credentials were not provided in api request |" f" {request.META.get('REMOTE_ADDR', 'Unknown IP')}" + ) + return APIResponse(False, {"detail": "Authentication credentials were not provided."}, status=status.HTTP_401_UNAUTHORIZED) + + if request.team_id and not request.team: + return APIResponse(False, {"detail": "Team not found."}, status=status.HTTP_404_NOT_FOUND) + + if request.team: + # Check for team permissions based on team_id and scopes + if not request.team.is_owner(token.user) and not request.team.is_logged_in_as_team(request): + team_permissions = TeamMemberPermission.objects.filter(team=request.team, user=token.user).first() + if not team_permissions or not all(scope in team_permissions.scopes for scope in scopes): + return APIResponse(False, {"detail": "Permission denied."}, status=status.HTTP_403_FORBIDDEN) + + # Check for global API Key permissions based on token scopes + if not all(scope in token.scopes for scope in scopes): + return APIResponse(False, {"detail": "Permission denied."}, status=status.HTTP_403_FORBIDDEN) + + token.update_last_used() + + return view_func(request, *args, **kwargs) + + _wrapped_view.required_scopes = scopes + return _wrapped_view + + return decorator diff --git a/src/core/api/public/endpoints/__init__.py b/src/core/api/public/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/public/endpoints/system_health.py b/src/core/api/public/endpoints/system_health.py new file mode 100644 index 0000000..6f01628 --- /dev/null +++ b/src/core/api/public/endpoints/system_health.py @@ -0,0 +1,63 @@ +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from django.db import connection, OperationalError +from django.core.cache import cache + +from rest_framework.decorators import api_view, permission_classes + +from core.api.public.permissions import IsSuperuser +from core.api.public.helpers.response import APIResponse + + +@swagger_auto_schema( + method="get", + operation_description="Check the system's health by verifying database and external API connections.", + responses={ + 200: openapi.Response( + description="System health check result", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "problems": openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "id": openapi.Schema(type=openapi.TYPE_STRING, description="Problem ID"), + "message": openapi.Schema(type=openapi.TYPE_STRING, description="Problem message"), + }, + ), + ), + "healthy": openapi.Schema(type=openapi.TYPE_BOOLEAN, description="Indicates overall system health"), + }, + ), + examples={ + "application/json": { + "problems": [ + {"id": "database", "message": "database failed to connect"}, + ], + "healthy": False, + } + }, + ) + }, +) +@api_view(["GET"]) +@permission_classes([IsSuperuser]) +def system_health_endpoint(request): + if not request.user or not request.user.is_superuser: + return APIResponse(False, "User is not permitted to view internal information", status=403) + + problems = [] + + try: + connection.ensure_connection() + except OperationalError: + problems.append({"id": "database", "message": "database failed to connect"}) + + try: + cache._cache.get_client().ping() + except ConnectionError: + problems.append({"id": "redis", "message": "redis failed to connect"}) + + return APIResponse({"problems": problems, "healthy": not bool(problems)}) diff --git a/src/core/api/public/endpoints/webhooks/__init__.py b/src/core/api/public/endpoints/webhooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/public/endpoints/webhooks/urls.py b/src/core/api/public/endpoints/webhooks/urls.py new file mode 100644 index 0000000..f4209f0 --- /dev/null +++ b/src/core/api/public/endpoints/webhooks/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from .webhook_task_queue_handler import webhook_task_queue_handler_view_endpoint + +urlpatterns = [ + path( + "receive/global/", + webhook_task_queue_handler_view_endpoint, + name="receive_global", + ) +] + +app_name = "webhooks" diff --git a/src/core/api/public/endpoints/webhooks/webhook_task_queue_handler.py b/src/core/api/public/endpoints/webhooks/webhook_task_queue_handler.py new file mode 100644 index 0000000..e317120 --- /dev/null +++ b/src/core/api/public/endpoints/webhooks/webhook_task_queue_handler.py @@ -0,0 +1,46 @@ +import logging +from core.api.public import APIAuthToken +from rest_framework.decorators import api_view + +from core.service.asyn_tasks.tasks import Task +from core.api.public.helpers.response import APIResponse + + +@api_view(["POST"]) +def webhook_task_queue_handler_view_endpoint(request): + token: APIAuthToken | None = request.auth + + if not token: + return APIResponse(False, {"status": "error", "message": "No token found"}, status=500) + + if not token.administrator_service_type == token.AdministratorServiceTypes.AWS_WEBHOOK_CALLBACK: + return APIResponse(False, {"status": "error", "message": "Invalid API key for this service"}, status=500) + + try: + data: dict = request.data + func_name: str = data.get("func_name") + args: list = data.get("args", []) + kwargs: dict = data.get("kwargs", {}) + + print(f"Function Name: {func_name}") + print(f"Arguments: {args}") + print(f"Keyword Arguments: {kwargs}") + + # Validate function name + if not func_name: + raise ValueError("Function name is required.") + + # Create an instance of Task + task_helper = Task() + + # Attempt to execute the function + result = task_helper.execute_now(func_name, *args, **kwargs) + + # Handle the result (e.g., store it or log it) + print(f"Webhook executed: {func_name} with result: {result}") + + return APIResponse(True, {"status": "success", "result": result}) + + except Exception as e: + logging.error(f"Error executing webhook task: {str(e)}") + return APIResponse(False, {"status": "error", "message": "An internal error has occurred."}, status=500) diff --git a/src/core/api/public/helpers/__init__.py b/src/core/api/public/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/public/helpers/deprecate.py b/src/core/api/public/helpers/deprecate.py new file mode 100644 index 0000000..54c4ee3 --- /dev/null +++ b/src/core/api/public/helpers/deprecate.py @@ -0,0 +1,46 @@ +import datetime +import functools +import logging + +from rest_framework.response import Response + +logger = logging.getLogger(__name__) + + +# add type hints for these deprecation dates + + +def deprecated(deprecation_date: datetime.datetime | None = None, end_of_life_date: datetime.datetime | None = None): + """ + Returns a decorator which informs requester that the decorated endpoint has been deprecated. + """ + + def decorator_deprecated(func): + """Amend the request with information that the endpoint has been deprecated and when it will be removed""" + + @functools.wraps(func) + def wrapper_deprecated(*args, **kwargs): + # do something before handling the request, could e.g. issue a django signal + logger.warning("Deprecated endpoint %s called", func.__name__) + + if end_of_life_date and datetime.datetime.now() > end_of_life_date: + return Response( + {"success": False, "message": "This endpoint is no longer available"}, + status=410, + headers={"X-Deprecated": "", "X-Deprecation-Date": deprecation_date, "X-End-Of-Life-Date": end_of_life_date}, + ) + + response: Response = func(*args, **kwargs) + + # amend the response with deprecation information + if isinstance(response, Response): + response.headers["X-Deprecated"] = "" + if deprecation_date: + response.headers["X-Deprecation-Date"] = deprecation_date + if end_of_life_date: + response.headers["X-End-Of-Life-Date"] = deprecation_date + return response + + return wrapper_deprecated + + return decorator_deprecated diff --git a/src/core/api/public/helpers/response.py b/src/core/api/public/helpers/response.py new file mode 100644 index 0000000..f0ae924 --- /dev/null +++ b/src/core/api/public/helpers/response.py @@ -0,0 +1,19 @@ +from rest_framework.response import Response + + +def APIResponse(success: bool = True, data: str | dict | None = None, meta=None, status: int = 0, **kwargs) -> Response: + """ + + Returns a rest_framework Response object, but prefills meta (success etc) aswell as the data with KWARGS. + + """ + meta = meta or {} + if not status and success: + status = 201 + elif not status: + status = 400 + + if success: + return Response({"meta": {"success": True, **meta}, "data": {**kwargs} | data if isinstance(data, dict) else {}}, status=status) + else: + return Response({"meta": {"success": False}, "error": data}, status=status) diff --git a/src/core/api/public/middleware.py b/src/core/api/public/middleware.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/public/models.py b/src/core/api/public/models.py new file mode 100644 index 0000000..8e97635 --- /dev/null +++ b/src/core/api/public/models.py @@ -0,0 +1,69 @@ +from django.db import models +from django.contrib.auth.hashers import check_password, make_password +import binascii +import os +from django.utils import timezone + +from core.models import OwnerBase, ExpiresBase + + +class APIAuthToken(OwnerBase, ExpiresBase): + id = models.AutoField(primary_key=True) + + hashed_key = models.CharField("Key", max_length=128, unique=True) + + name = models.CharField("Key Name", max_length=64) + description = models.TextField("Description", blank=True, null=True) + created = models.DateTimeField("Created", auto_now_add=True) + last_used = models.DateTimeField("Last Used", null=True, blank=True) + # expires = models.DateTimeField("Expires", null=True, blank=True, help_text="Leave blank for no expiry") + # expired = models.BooleanField("Expired", default=False, help_text="If the key has expired") + # active = models.BooleanField("Active", default=True, help_text="If the key is active") + scopes = models.JSONField("Scopes", default=list, help_text="List of permitted scopes") + + class AdministratorServiceTypes(models.TextChoices): + AWS_WEBHOOK_CALLBACK = "aws_webhook_callback", "AWS Webhook Callback" + AWS_API_DESTINATION = "aws_api_destination", "AWS API Destination" + + administrator_service_type = models.CharField("Administrator Service Type", max_length=64, blank=True, null=True) + + class Meta: + verbose_name = "API Key" + verbose_name_plural = "API Keys" + app_label = 'core' + + def __str__(self): + return self.name + + def update_last_used(self): + self.last_used = timezone.now() + self.save() + return True + + # def save(self, *args, **kwargs): + # return super().save(*args, **kwargs) + + def generate_key(self) -> str: + """ + :returns: raw_key + """ + + raw = binascii.hexlify(os.urandom(20)).decode() + self.hashed_key = self.hash_raw_key(raw) + + return raw + + @classmethod + def hash_raw_key(cls, raw_key: str): + return make_password(raw_key, salt="api_tokens", hasher="default") + + def verify(self, key) -> bool: + return check_password(key, self.hashed_key) + + def deactivate(self): + self.active = False + self.save() + return self + + def has_scope(self, scope): + return scope in self.scopes diff --git a/src/core/api/public/permissions.py b/src/core/api/public/permissions.py new file mode 100644 index 0000000..9893a41 --- /dev/null +++ b/src/core/api/public/permissions.py @@ -0,0 +1,66 @@ +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from django.conf import settings + +SCOPES = { + "clients:read", + "clients:write", + "invoices:read", + "invoices:write", + "receipts:read", + "receipts:write", + "clients:read", + "clients:write", + "emails:read", + "emails:send", + "profile:read", + "profile:write", + "api_keys:read", + "api_keys:write", + "team_permissions:read", + "team_permissions:write", + "team:invite", + "team:kick", + "account_defaults:write", + "email_templates:read", + "email_templates:write", +} + +SCOPES_TREE = { + "clients:read": {"clients:read"}, + "clients:write": {"clients:read", "clients:write"}, + "invoices:read": {"invoices:read"}, + "invoices:write": {"invoices:read", "invoices:write"}, + "profile:read": {"profile:read"}, + "profile:write": {"profile:read", "profile:write"}, + "api_keys:read": {"api_keys:read"}, + "api_keys:write": {"api_keys:read", "api_keys:write"}, + "team_permissions:read": {"team_permissions:read"}, + "team_permissions:write": {"team_permissions:read", "team_permissions:write"}, + "team:invite": {"team:invite"}, + "team:kick": {"team:kick", "team:invite"}, + "email_templates:read": {"email_templates:read"}, + "email_templates:write": {"email_templates:read", "email_templates:write"}, + "account_defaults:write": {"account_defaults:write"}, +} + +SCOPE_DESCRIPTIONS = { + "clients": {"description": "Access customer details", "options": {"read": "Read only", "write": "Read and write"}}, + "invoices": {"description": "Access invoices", "options": {"read": "Read only", "write": "Read and write"}}, + "profile": {"description": "Access profile details", "options": {"read": "Read only", "write": "Read and write"}}, + "api_keys": {"description": "Access API keys", "options": {"read": "Read only", "write": "Read and write"}}, + "team_permissions": {"description": "Access team permissions", "options": {"read": "Read only", "write": "Read and write"}}, + "team": {"description": "Invite team members", "options": {"invite": "Invite members"}}, + "email_templates": {"description": "Access email templates", "options": {"read": "Read only", "write": "Read and write"}}, + "account_defaults": {"description": "Modify account defaults", "options": {"write": "Read and write"}}, +} + +if settings.BILLING_ENABLED: + SCOPES.add("billing:manage") + SCOPES_TREE["billing:manage"] = {"billing:manage"} + SCOPE_DESCRIPTIONS["billing"] = {"description": "Access billing details + stripe", "options": {"manage": "Manage billing"}} + + +class IsSuperuser(BasePermission): + def has_permission(self, request: Request, view: object) -> bool: + return bool(request.user and request.user.is_superuser) diff --git a/src/core/api/public/serializers/__init__.py b/src/core/api/public/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/public/swagger_ui.py b/src/core/api/public/swagger_ui.py new file mode 100644 index 0000000..e1b2f33 --- /dev/null +++ b/src/core/api/public/swagger_ui.py @@ -0,0 +1,42 @@ +from django.urls import path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions + +INFO = openapi.Info( + title="MyFinances Public API", + default_version="v0.0.1", + description="", + terms_of_service="", + contact=openapi.Contact(email="support@strelix.org"), + license=openapi.License(name="AGPL v3"), +) + +schema_view = get_schema_view( + INFO, + public=True, + permission_classes=[permissions.AllowAny], +) + + +def get_swagger_ui(): + return schema_view + + +def get_swagger_endpoints(debug): + return ( + [ + path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), + ] + + [ + path("swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json"), + path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), + ] + if debug + else [] + ) + + +TEAM_PARAMETER = openapi.Parameter( + "team_id", openapi.IN_QUERY, description="id of the team you want to do this action under", type=openapi.TYPE_STRING, required=False +) diff --git a/src/core/api/public/types.py b/src/core/api/public/types.py new file mode 100644 index 0000000..c73f186 --- /dev/null +++ b/src/core/api/public/types.py @@ -0,0 +1,12 @@ +from rest_framework.request import Request + +from core.api.public import APIAuthToken +from core.models import User, Organization + + +class APIRequest(Request): + user: User + auth: APIAuthToken + api_token: APIAuthToken + team: Organization | None + team_id: int | None diff --git a/src/core/api/public/urls.py b/src/core/api/public/urls.py new file mode 100644 index 0000000..7c2d6c7 --- /dev/null +++ b/src/core/api/public/urls.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from django.conf.urls import include +from django.urls import path, re_path +from rest_framework.authentication import TokenAuthentication + +from .endpoints.system_health import system_health_endpoint + +INTERNAL_URLS = [path("health/", system_health_endpoint, name="public-system-health")] + +urlpatterns = [ + path("internal/", include(INTERNAL_URLS)), + # path("clients/", include("core.api.public.endpoints.clients.urls")), + # path("invoices/", include("core.api.public.endpoints.Invoices.urls")), + path("webhooks/", include("core.api.public.endpoints.webhooks.urls")), +] + +app_name = "public" diff --git a/src/core/api/quotas/__init__.py b/src/core/api/quotas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/quotas/fetch.py b/src/core/api/quotas/fetch.py new file mode 100644 index 0000000..716d3a3 --- /dev/null +++ b/src/core/api/quotas/fetch.py @@ -0,0 +1,30 @@ +from django.db.models import Q +from django.shortcuts import render, redirect + +from core.models import QuotaLimit +from core.types.htmx import HtmxHttpRequest + + +def fetch_all_quotas(request: HtmxHttpRequest, group: str): + context = {} + if not request.htmx: + return redirect("quotas") + + search_text = request.GET.get("search") + + results = QuotaLimit.objects.filter(slug__startswith=group).prefetch_related("quota_overrides", "quota_usage").order_by("-slug") + + if search_text: + results = results.filter(Q(name__icontains=search_text)) + + quotas = [ + { + "quota_limit": ql.get_quota_limit(request.user), + "period_usage": ql.get_period_usage(request.user), + "quota_object": ql, + } + for ql in results + ] + + context.update({"quotas": quotas}) + return render(request, "pages/quotas/_fetch_body.html", context) diff --git a/src/core/api/quotas/requests.py b/src/core/api/quotas/requests.py new file mode 100644 index 0000000..4daeb94 --- /dev/null +++ b/src/core/api/quotas/requests.py @@ -0,0 +1,136 @@ +from dataclasses import dataclass +from typing import Union + +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods + +from core.decorators import superuser_only +from core.models import QuotaIncreaseRequest, QuotaLimit, QuotaUsage, QuotaOverrides +from core.types.htmx import HtmxHttpRequest + + +# from backend.utils.quota_limit_ops import quota_usage_check_under + + +def submit_request(request: HtmxHttpRequest, slug) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + + new_value = request.POST.get("new_value", "") + reason = request.POST.get("reason", "") + + try: + quota_limit = QuotaLimit.objects.get(slug=slug) + except QuotaLimit.DoesNotExist: + return error(request, "Failed to get the quota limit type") + + # usage_per_item = quota_usage_check_under(request, "quota_increase-request", extra_data=quota_limit.id, api=True, htmx=True) + # usage_per_month = quota_usage_check_under( + # request, "quota_increase-requests_per_month_per_quota", extra_data=quota_limit.id, api=True, htmx=True + # ) + + # if not isinstance(usage_per_item, bool): + # return usage_per_item + + # if not isinstance(usage_per_month, bool): + # return usage_per_month + + current = quota_limit.get_quota_limit(request.user) + + validate = validate_request(new_value, reason, current) + + if isinstance(validate, Error): + return error(request, validate.message) + + quota_increase_request = QuotaIncreaseRequest.objects.create( + user=request.user, quota_limit=quota_limit, new_value=new_value, current_value=current, reason=reason + ) + + QuotaUsage.create_str(request.user, "quota_increase-request", quota_increase_request.id) + QuotaUsage.create_str(request.user, "quota_increase-requests_per_month_per_quota", quota_limit.id) + + messages.success(request, "Successfully submitted a quota increase request") + return render(request, "base/toast.html") + + +@dataclass +class Error: + message: str + + +def error(request: HtmxHttpRequest, message: str) -> HttpResponse: + messages.error(request, message) + return render(request, "partials/messages_list.html") + + +def validate_request(new_value, reason, current) -> Union[bool, Error]: + if not new_value: + return Error("Please enter a valid increase value") + + try: + new_value = int(new_value) + if new_value <= current: + raise ValueError + except ValueError: + return Error("Please enter a valid increase value that is above your current limit.") + + if len(reason) < 25: + return Error("Please enter a valid reason for the increase.") + + return True + + +@superuser_only +@require_http_methods(["DELETE", "POST"]) +def approve_request(request: HtmxHttpRequest, request_id) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + try: + quota_request = QuotaIncreaseRequest.objects.get(id=request_id) + except QuotaIncreaseRequest.DoesNotExist: + return error(request, "Failed to get the quota increase request") + + try: + quota_override_existing = QuotaOverrides.objects.get(user=quota_request.user, quota_limit=quota_request.quota_limit) + quota_override_existing.value = quota_request.new_value + quota_override_existing.save() + except QuotaOverrides.DoesNotExist: + QuotaOverrides.objects.create( + user=quota_request.user, + value=quota_request.new_value, + quota_limit=quota_request.quota_limit, + ) + + quota_limit_for_increase = QuotaLimit.objects.get(slug="quota_increase-request") + QuotaUsage.objects.filter(user=quota_request.user, quota_limit=quota_limit_for_increase, extra_data=quota_request.id).delete() + quota_request.status = "approved" + quota_request.save() + + try: + QuotaUsage.objects.get( + quota_limit=QuotaLimit.objects.get(slug="quota_increase-requests_per_month_per_quota"), extra_data=quota_request.quota_limit_id + ).delete() + except QuotaUsage.DoesNotExist: + ... + + return HttpResponse(status=200) + + +@superuser_only +@require_http_methods(["DELETE", "POST"]) +def decline_request(request: HtmxHttpRequest, request_id) -> HttpResponse: + if not request.htmx: + return redirect("quotas") + try: + quota_request = QuotaIncreaseRequest.objects.get(id=request_id) + except QuotaIncreaseRequest.DoesNotExist: + return error(request, "Failed to get the quota increase request") + + quota_limit_for_increase = QuotaLimit.objects.get(slug="quota_increase-request") + QuotaUsage.objects.filter(user=quota_request.user, quota_limit=quota_limit_for_increase, extra_data=quota_request.id).delete() + quota_request.status = "decline" + quota_request.save() + + return HttpResponse(status=200) diff --git a/src/core/api/quotas/urls.py b/src/core/api/quotas/urls.py new file mode 100644 index 0000000..7a573eb --- /dev/null +++ b/src/core/api/quotas/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +# from . import fetch, requests + +urlpatterns = [ + # path( + # "fetch//", + # fetch.fetch_all_quotas, + # name="fetch", + # ), + # path("submit_request//", requests.submit_request, name="submit_request"), + # path("request//approve/", requests.approve_request, name="approve request"), + # path("request//decline/", requests.decline_request, name="decline request"), +] + +app_name = "quotas" diff --git a/src/core/api/settings/__init__.py b/src/core/api/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/settings/api_keys.py b/src/core/api/settings/api_keys.py new file mode 100644 index 0000000..b04ae0d --- /dev/null +++ b/src/core/api/settings/api_keys.py @@ -0,0 +1,69 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + +from core.api.public import APIAuthToken +from core.service.api_keys.delete import delete_api_key +from core.service.api_keys.generate import generate_public_api_key +from core.service.api_keys.get import get_api_key_by_id +from core.service.permissions.scopes import get_permissions_from_request + +from core.types.requests import WebRequest +from core.decorators import web_require_scopes + + +@require_http_methods(["POST"]) +@web_require_scopes("api_keys:write") +def generate_api_key_endpoint(request: WebRequest) -> HttpResponse: + name = request.POST.get("name") + expires = request.POST.get("expires") + description = request.POST.get("description") + administrator_toggle = True if request.POST.get("administrator") == "on" else False + administrator_type = request.POST.get("administrator_type") + + permissions: list = get_permissions_from_request(request) + + key_obj, key_response = generate_public_api_key( + request, + request.user.logged_in_as_team or request.user, + name, + permissions, + expires=expires, + description=description, + administrator_toggle=administrator_toggle, + administrator_type=administrator_type, + ) + + if not key_obj: + messages.error(request, key_response) + return render(request, "base/toast.html") + + messages.success(request, "API key generated successfully") + + http_response = render( + request, + "pages/settings/settings/api_key_generated_response.html", + { + "raw_key": key_response, + "name": name, + }, + ) + + http_response.headers["HX-Reswap"] = "beforebegin" + http_response.headers["HX-Retarget"] = 'div[data-hx-container="api_keys"]' + + return http_response + + +@require_http_methods(["DELETE"]) +def revoke_api_key_endpoint(request: WebRequest, key_id: str) -> HttpResponse: + key: APIAuthToken | None = get_api_key_by_id(request.user.logged_in_as_team or request.user, key_id) + + delete_key_response = delete_api_key(request, request.user.logged_in_as_team or request.user, key=key) + + if isinstance(delete_key_response, str): + messages.error(request, "This key does not exist") + else: + messages.success(request, "Successfully revoked the API Key") + return render(request, "base/toast.html") diff --git a/src/core/api/settings/change_name.py b/src/core/api/settings/change_name.py new file mode 100644 index 0000000..09275d4 --- /dev/null +++ b/src/core/api/settings/change_name.py @@ -0,0 +1,40 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + +from core.types.htmx import HtmxHttpRequest + + +@require_http_methods(["POST"]) +def change_account_name(request: HtmxHttpRequest): + if not request.htmx: + return HttpResponse("Invalid Request", status=405) + + htmx_return = "base/toasts.html" + + first_name = request.POST.get("first_name") + last_name = request.POST.get("last_name") + + if not first_name and not last_name: + messages.error(request, "Please enter a valid firstname or lastname.") + return render(request, htmx_return) + + if request.user.first_name == first_name and request.user.last_name == last_name: + messages.warning(request, "You already have this name.") + return render(request, htmx_return) + + if first_name: + request.user.first_name = first_name + + if last_name: + request.user.last_name = last_name + + request.user.save() + + messages.success( + request, + f"Successfully changed your name to {request.user.get_full_name()}", + ) + + return render(request, htmx_return) diff --git a/src/core/api/settings/preferences.py b/src/core/api/settings/preferences.py new file mode 100644 index 0000000..df229c7 --- /dev/null +++ b/src/core/api/settings/preferences.py @@ -0,0 +1,48 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + +from core.models import UserSettings + + +@require_http_methods(["POST"]) +def update_account_preferences(request): + currency = request.POST.get("currency", None) + try: + usersettings = request.user.user_profile + except UserSettings.DoesNotExist: + usersettings = UserSettings.objects.create(user=request.user) + + htmx_return = "base/toasts.html" + + if not request.htmx and not currency: + return HttpResponse("Invalid Request", status=400) + elif not currency or currency not in usersettings.CURRENCIES: + messages.error(request, "Invalid Currency") + return render(request, htmx_return) + + usersettings.currency = currency + + updated_features: bool = False + + for choice, _ in usersettings.CoreFeatures.choices: + selected: str | None = request.POST.get(f"selected_{choice}", None) + + if choice in usersettings.disabled_features: # currently disabled + if selected: # enabled + updated_features = True + usersettings.disabled_features.remove(choice) + else: + if not selected: # disabled + updated_features = True + usersettings.disabled_features.append(choice) + + usersettings.save(update_fields=["disabled_features", "currency"]) + messages.success(request, "Successfully updated preferences") + + if updated_features: + response = HttpResponse("Success") + response["HX-Refresh"] = "true" + return response + return render(request, htmx_return) diff --git a/src/core/api/settings/profile_picture.py b/src/core/api/settings/profile_picture.py new file mode 100644 index 0000000..4dfa53f --- /dev/null +++ b/src/core/api/settings/profile_picture.py @@ -0,0 +1,29 @@ +from django.contrib import messages +from django.shortcuts import redirect, render +from django.views.decorators.http import require_http_methods + +from core.service.settings.update import update_profile_picture, UpdateProfilePictureServiceResponse +from core.service.settings.view import get_user_profile +from core.types.requests import WebRequest + + +@require_http_methods(["POST"]) +def change_profile_picture_endpoint(request: WebRequest): + if not request.htmx: + messages.error(request, "Invalid request") + return redirect("core:settings:dashboard with page", page="profile") + + user_profile = get_user_profile(request) + + update_response: UpdateProfilePictureServiceResponse = update_profile_picture(request.FILES.get("profile_picture_image"), user_profile) + + if update_response.success: + messages.success(request, update_response.response) + else: + messages.error(request, update_response.error) + + return render( + request, + "pages/settings/settings/_post_profile_pic.html", + {"users_profile_picture": user_profile.profile_picture_url}, + ) diff --git a/src/core/api/settings/urls.py b/src/core/api/settings/urls.py new file mode 100644 index 0000000..c79d424 --- /dev/null +++ b/src/core/api/settings/urls.py @@ -0,0 +1,22 @@ +from django.urls import path + +from . import change_name, profile_picture, preferences +from .api_keys import generate_api_key_endpoint, revoke_api_key_endpoint + +urlpatterns = [ + path( + "account_preferences/", + preferences.update_account_preferences, + name="account_preferences", + ), + path( + "change_name/", + change_name.change_account_name, + name="change_name", + ), + path("profile_picture/", profile_picture.change_profile_picture_endpoint, name="update profile picture"), + path("api_keys/generate/", generate_api_key_endpoint, name="api_keys generate"), + path("api_keys/revoke//", revoke_api_key_endpoint, name="api_keys revoke"), +] + +app_name = "settings" diff --git a/src/core/api/teams/__init__.py b/src/core/api/teams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/api/teams/create.py b/src/core/api/teams/create.py new file mode 100644 index 0000000..2bb3a1e --- /dev/null +++ b/src/core/api/teams/create.py @@ -0,0 +1,36 @@ +from django.contrib import messages +from django.shortcuts import render +from django.views.decorators.http import require_POST + +from core.decorators import has_entitlements +from core.models import Organization +from core.types.htmx import HtmxHttpRequest + + +@require_POST +@has_entitlements("organizations") +# @quota_usage_check("teams-count", api=True, htmx=True) +def create_team(request: HtmxHttpRequest): + name = request.POST.get("name") + + if not name: + messages.error(request, "A team name field must be filled.") + return render(request, "partials/messages_list.html") + + if Organization.objects.filter(name=name).exists(): + messages.error(request, "A team with this name already exists.") + return render(request, "partials/messages_list.html") + + team = Organization.objects.create(name=name, leader=request.user) + + # QuotaUsage.create_str(request.user, "teams-count", team.id) + # QuotaUsage.create_str(request.user, "teams-joined", team.id) + + if not request.user.logged_in_as_team: + request.user.logged_in_as_team = team + request.user.save() + + messages.success(request, f"Successfully created team {name} with the ID of #{team.id}") + response = render(request, "partials/messages_list.html") + response["HX-Refresh"] = "true" + return response diff --git a/src/core/api/teams/create_user.py b/src/core/api/teams/create_user.py new file mode 100644 index 0000000..ed2f853 --- /dev/null +++ b/src/core/api/teams/create_user.py @@ -0,0 +1,37 @@ +from django.contrib import messages +from django.shortcuts import render + +from core.decorators import web_require_scopes +from core.models import Organization +from core.service.permissions.scopes import get_permissions_from_request +from core.service.teams.create_user import create_user_service +from core.types.requests import WebRequest + + +@web_require_scopes("team:invite", True, True) +def create_user_endpoint(request: WebRequest): + team_id = request.POST.get("team_id", "") + + team: Organization | None = Organization.objects.filter(id=team_id).first() + + if not team: + messages.error(request, "This team does not exist") + return render(request, "base/toast.html") + + if not team.is_owner(request.user): + messages.error(request, "Only the team owner can create users") + return render(request, "base/toast.html") + + first_name = request.POST.get("first_name", "") + last_name = request.POST.get("last_name", "") + email = request.POST.get("email", "") + permissions: list = get_permissions_from_request(request) + + created_user = create_user_service(request, email, team, first_name, last_name, permissions) + + if created_user.failed: + messages.error(request, created_user.error) + return render(request, "base/toast.html") + else: + messages.success(request, f"The account for {first_name} was created successfully. They have been emailed instructions.") + return render(request, "base/toast.html") diff --git a/src/core/api/teams/edit_permissions.py b/src/core/api/teams/edit_permissions.py new file mode 100644 index 0000000..58e811d --- /dev/null +++ b/src/core/api/teams/edit_permissions.py @@ -0,0 +1,31 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + +from core.decorators import web_require_scopes +from core.models import User +from core.service.permissions.scopes import get_permissions_from_request +from core.service.teams.permissions import edit_member_permissions +from core.types.requests import WebRequest + + +@require_http_methods(["POST"]) +@web_require_scopes("team_permissions:write") +def edit_user_permissions_endpoint(request: WebRequest) -> HttpResponse: + permissions: list = get_permissions_from_request(request) + user_id = request.POST.get("user_id") + + receiver: User | None = User.objects.filter(id=user_id).first() + + if not receiver: + messages.error(request, "Invalid user") + return render(request, "base/toast.html") + + edit_response = edit_member_permissions(receiver, request.user.logged_in_as_team, permissions) + + if edit_response.success: + messages.success(request, "User permissions saved successfully") + else: + messages.error(request, edit_response.error) + return render(request, "base/toast.html") diff --git a/src/core/api/teams/invites.py b/src/core/api/teams/invites.py new file mode 100644 index 0000000..8aa4b48 --- /dev/null +++ b/src/core/api/teams/invites.py @@ -0,0 +1,193 @@ +from textwrap import dedent + +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render +from django.urls import reverse + +from core.decorators import web_require_scopes +from core.utils.settings_helper import send_email +from core.models import Notification, Organization, TeamInvitation, User +from core.types.htmx import HtmxHttpRequest + +def delete_notification(user: User, code: TeamInvitation): + notification = Notification.objects.filter( + user=user, + message="New Organization Invite", + action="modal", + action_value="accept_invite", + extra_type="accept_invite_with_code", + extra_value=code, + ).first() + + if notification: + notification.delete() + + +def check_team_invitation_is_valid(request, invitation: TeamInvitation, code=None): + valid: bool = True + + if not invitation.is_active(): + valid = False + messages.error(request, "Invitation has expired") + + if not valid: + delete_notification(request.user, code) + return False + + return True + + +@web_require_scopes("team:invite", True, True) +def send_user_team_invite(request: HtmxHttpRequest): + user_email = request.POST.get("email") + team_id = request.POST.get("team_id", "") + team: Organization | None = Organization.objects.filter(leader=request.user, id=team_id).first() + + def return_error_notif(request: HtmxHttpRequest, message: str, autohide=None): + messages.error(request, message) + context = {"autohide": False} if autohide is False else {} + resp = render(request, "partials/messages_list.html", context=context, status=200) + resp["HX-Trigger-After-Swap"] = "invite_user_error" + return resp + + if not user_email: + return return_error_notif(request, "Please enter a valid user email") + + if not team: + return return_error_notif(request, "You are not the leader of this team") + + user: User | None = User.objects.filter(email=user_email).first() + + if not user: + return return_error_notif(request, 'User not found. Either ask them to create an account or press "Create User"') + + if user.teams_joined.filter(pk=team_id).exists(): + return return_error_notif(request, "User already is in this team") + + invitation = TeamInvitation.objects.create(team=team, user=user, invited_by=request.user) + + Notification.objects.create( + user=user, + message=f"New Organization Invite", + action="modal", + action_value="accept_invite", + extra_type="accept_invite_with_code", + extra_value=invitation.code, + ) + + send_email( + destination=user.email, + subject="New Organization Invite", + content=dedent( + f""" + Hi {user.first_name or "User"}, + + {request.user.first_name or f"User {request.user.email}"} has invited you to join the organization \"{team.name}\" (#{team.id}) + + + Click the url below to accept the invite! + {request.build_absolute_uri(reverse("core:api:teams:join accept", kwargs={"code": invitation.code}))} + + Didn't give permission to be added to this organization? You can safely ignore the email, no actions can be done on + behalf of you without your action. + """ + ), + ) + + messages.success(request, "Invitation successfully sent") + response = HttpResponse(status=200) + response["HX-Refresh"] = "true" + return response + + +def accept_team_invite(request: HtmxHttpRequest, code): + invitation: TeamInvitation | None = TeamInvitation.objects.filter(code=code).prefetch_related("team").first() + + if not invitation: + messages.error(request, "Invalid Invite Code") + # Force break early to avoid "no invitation" on invitation.code + delete_notification(request.user, code) + return render(request, "partials/messages_list.html") + + if not check_team_invitation_is_valid(request, invitation, code): + messages.error(request, "Invalid invite - Maybe it has expired?") + return render(request, "partials/messages_list.html") + + if request.user.teams_joined.filter(pk=invitation.team_id).exists(): + messages.error(request, "You are already in this team") + response = render(request, "partials/messages_list.html", status=200) + response["HX-Trigger-After-Swap"] = "accept_invite_error" + return response + + invitation.team.members.add(request.user) + + notification = Notification.objects.filter( + user=request.user, + action="modal", + action_value="accept_invite", + extra_type="accept_invite_with_code", + extra_value=code, + ).first() + + if notification: + notification.delete() + + Notification.objects.create( + user=request.user, + message=f"You have now joined the team {invitation.team.name}", + action="normal", + ) + + Notification.objects.create( + user=invitation.invited_by, + message=f"{request.user.username} has joined your team", + action="normal", + ) + + invitation.delete() + + messages.success(request, f"You have successfully joined the team {invitation.team.name}") + response = HttpResponse(status=200) + response["HX-Refresh"] = "true" + return response + # return render(request, "partials/messages_list.html") + + +def decline_team_invite(request: HtmxHttpRequest, code): + invitation: TeamInvitation | None = TeamInvitation.objects.filter(code=code).first() + confirmation_text = request.POST.get("confirmation_text") + + if not invitation: + messages.error(request, "Invalid Invite Code") + # Force break early to avoid "no invitation" on invitation.code + delete_notification(request.user, code) + return render(request, "partials/messages_list.html") + + if not check_team_invitation_is_valid(request, invitation, code): + return render(request, "partials/messages_list.html") + + # if confirmation_text != "i confirm i want to decline " + invitation.team.name: + # messages.error(request, "Invalid confirmation text") + # return redirect("teams:dashboard join", code=code) # kwargs={"code": code}) + + invitation.team.members.remove(request.user) + + Notification.objects.create( + user=request.user, + message=f"You have declined the team invitation", + action="normal", + ) + + Notification.objects.create( + user=invitation.invited_by, + message=f"{request.user.username} has declined the team invitation", + action="normal", + ) + + delete_notification(request.user, code) + + invitation.delete() + messages.success(request, "You have successfully declined the team invitation") + + return render(request, "partials/messages_list.html") diff --git a/src/core/api/teams/kick.py b/src/core/api/teams/kick.py new file mode 100644 index 0000000..1ae0f62 --- /dev/null +++ b/src/core/api/teams/kick.py @@ -0,0 +1,33 @@ +from django.contrib import messages +from django.http import HttpRequest +from django.shortcuts import redirect + +from core.decorators import web_require_scopes +from core.models import User, Organization + + +@web_require_scopes("team:kick", True, True) +def kick_user(request: HttpRequest, user_id): + user: User | None = User.objects.filter(id=user_id).first() + confirmation_text = request.POST.get("confirmation_text") + if not user: + messages.error(request, "User not found") + return redirect("core:teams:dashboard") + + if confirmation_text != f"i confirm i want to kick {user.username}": + messages.error(request, "Invalid confirmation") + return redirect("core:teams:dashboard") + + team: Organization | None = user.teams_joined.first() + if not team: + messages.error(request, "User is not apart of your team") + return redirect("core:teams:dashboard") + + if team.leader != request.user: + messages.error(request, "You don't have the required permissions to kick this user") + return redirect("core:teams:dashboard") + + team.members.remove(user) + messages.success(request, f"Successfully kicked {user.username}") + + return redirect("core:teams:dashboard") diff --git a/src/core/api/teams/leave.py b/src/core/api/teams/leave.py new file mode 100644 index 0000000..2625103 --- /dev/null +++ b/src/core/api/teams/leave.py @@ -0,0 +1,32 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render + +from core.models import Organization +from core.types.htmx import HtmxHttpRequest + + +def return_error_notif(request: HtmxHttpRequest, message: str): + messages.error(request, message) + resp = render(request, "partials/messages_list.html", status=200) + resp["HX-Trigger-After-Swap"] = "leave_team_error" + return resp + + +def leave_team_confirmed(request: HtmxHttpRequest, team_id): + team: Organization | None = Organization.objects.filter(id=team_id).first() + + if not team: + return return_error_notif(request, "Team not found") + + if team.leader == request.user: # may be changed in the future. If no members allow delete + return return_error_notif(request, "You cannot leave your own team") + + if request.user.teams_joined.filter(id=team_id).exists(): + team.members.remove(request.user) + messages.success(request, f"You have successfully left the team {team.name}") + response = HttpResponse(status=200) + response["HX-Refresh"] = "true" + return response + else: + return return_error_notif(request, "You are not a member of this team") diff --git a/src/core/api/teams/switch_team.py b/src/core/api/teams/switch_team.py new file mode 100644 index 0000000..ee61fd4 --- /dev/null +++ b/src/core/api/teams/switch_team.py @@ -0,0 +1,53 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render + +from core.models import Organization +from core.types.htmx import HtmxHttpRequest + + +def switch_team(request: HtmxHttpRequest, team_id: str | int | None = None): + if not team_id: + team_id = request.POST.get("join_team", None) + + if not team_id: + if not request.user.logged_in_as_team: + messages.warning(request, "You are not logged into an organization") + else: + messages.success(request, "You are now logged into your personal account") + + request.user.logged_in_as_team = None + request.user.save() + response = HttpResponse(status=200) + response["HX-Refresh"] = "true" + return response + + team: Organization | None = Organization.objects.filter(id=team_id).first() + + if not team: + messages.error(request, "Team not found") + return render(request, "partials/messages_list.html") + + if request.user.logged_in_as_team == team: + messages.error(request, "You are already logged in for this team") + return render(request, "partials/messages_list.html") + + if not request.user.teams_leader_of.filter(id=team_id).exists() and not request.user.teams_joined.filter(id=team_id).exists(): + messages.error(request, "You are not a member of this team") + return render(request, "partials/messages_list.html") + + messages.success(request, f"Now signing into the organization '{team.name}'") + request.user.logged_in_as_team = team + request.user.save() + + response = HttpResponse(status=200) + response["HX-Refresh"] = "true" + return response + # return render(request, "components/+logged_in_for.html") + + +def get_dropdown(request: HtmxHttpRequest): + if not request.htmx: + return HttpResponse("Invalid Request", status=405) + + return render(request, "base/topbar/_organizations_list.html") diff --git a/src/core/api/teams/urls.py b/src/core/api/teams/urls.py new file mode 100644 index 0000000..ef69884 --- /dev/null +++ b/src/core/api/teams/urls.py @@ -0,0 +1,58 @@ +from django.urls import path + +from . import kick, switch_team, invites, leave, create, edit_permissions +from .create_user import create_user_endpoint + +urlpatterns = [ + path("edit_permissions/", edit_permissions.edit_user_permissions_endpoint, name="edit_permissions"), + path( + "kick/", + kick.kick_user, + name="kick", + ), + path( + "switch_team//", + switch_team.switch_team, + name="switch_team", + ), + path( + "switch_team/", + switch_team.switch_team, + name="switch_team input", + ), + path( + "create_user/", + create_user_endpoint, + name="create_user", + ), + # INVITES # + path( + "invite/", + invites.send_user_team_invite, + name="invite", + ), + path( + "join//accept/", + invites.accept_team_invite, + name="join accept", + ), + path( + "join//decline/", + invites.decline_team_invite, + name="join decline", + ), + # LEAVE TEAM # + path( + "leave//confirm/", + leave.leave_team_confirmed, + name="leave confirm", + ), + path( + "create/", + create.create_team, + name="create", + ), + path("get_dropdown/", switch_team.get_dropdown, name="get_dropdown"), +] + +app_name = "teams" diff --git a/src/core/api/urls.py b/src/core/api/urls.py new file mode 100644 index 0000000..b0cdc81 --- /dev/null +++ b/src/core/api/urls.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from django.urls import include +from django.urls import path + +urlpatterns = [ + path("base/", include("core.api.base.urls")), + path("teams/", include("core.api.teams.urls")), + path("settings/", include("core.api.settings.urls")), + path("quotas/", include("core.api.quotas.urls")), + # path("clients/", include("backend_utils.clients.api.urls")), + path("emails/", include("core.api.emails.urls")), + path("maintenance/", include("core.api.maintenance.urls")), + path("landing_page/", include("core.api.landing_page.urls")), + path("public/", include("core.api.public.urls")), +# path("", include("backend_utils.finance.api.urls")), +] + +app_name = "api" diff --git a/src/core/apps.py b/src/core/apps.py new file mode 100644 index 0000000..f9da933 --- /dev/null +++ b/src/core/apps.py @@ -0,0 +1,15 @@ +import importlib + +from django.apps import AppConfig + +class CoreConfig(AppConfig): + name = 'core' + verbose_name = 'Strelix Core' + + DEFAULT_AUTHENTICATION_CLASSES = "core.api.public.authentication.CustomBearerAuthentication" + + def ready(self): + from core.api.public.models import APIAuthToken + import core.signals + importlib.import_module('core.service.modals.modals') + pass \ No newline at end of file diff --git a/src/core/backend_utils/__init__.py b/src/core/backend_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/boto3/__init__.py b/src/core/boto3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/boto3/handler.py b/src/core/boto3/handler.py new file mode 100644 index 0000000..d9a64fd --- /dev/null +++ b/src/core/boto3/handler.py @@ -0,0 +1,86 @@ +import botocore.client +from botocore.config import Config +from botocore.exceptions import NoCredentialsError, PartialCredentialsError + +from settings.helpers import get_var + +import boto3 +import logging + +logger = logging.getLogger(__name__) + +DEBUG_LEVEL = get_var("AWS_LOG_LEVEL", default="debug") +DEBUG_LEVEL = "debug" if DEBUG_LEVEL == "debug" else "info" if DEBUG_LEVEL == "info" else None + + +class Boto3Handler: + def __init__(self): + self.initiated: bool = False + self.region_name: str = get_var("AWS_REGION_NAME", "eu-west-2") + self.aws_access_key_id: str = get_var("AWS_ACCESS_KEY_ID") + self.aws_access_key_secret: str = get_var("AWS_ACCESS_KEY") + self.scheduler_lambda_arn: str = get_var("SCHEDULER_LAMBDA_ARN") + self.scheduler_lambda_access_role_arn: str = get_var("SCHEDULER_LAMBDA_ACCESS_ROLE_ARN") + self.scheduler_invoices_group_name: str = get_var("SCHEDULER_INVOICES_GROUP_NAME") + self.dynamodb_client = None + self.scheduler_client = None + + print("BOTO3 SETTINGS") + print(f"Region: {self.region_name}") + print("| has aws access key id" if self.aws_access_key_id else "X no aws access key id") + print("| has aws access key secret" if self.aws_access_key_secret else "X no aws access key secret") + print("| has scheduler lambda arn" if self.scheduler_lambda_arn else "X no scheduler lambda arn") + print( + "| has scheduler lambda access role arn" if self.scheduler_lambda_access_role_arn else "X no scheduler lambda access role arn" + ) + print("| has scheduler invoices group name" if self.scheduler_invoices_group_name else "X no scheduler invoices group name") + print("END OF BOTO3 SETTINGS") + + self._initiate_clients() + + def _initiate_session(self): + self._boto3_config = Config(region_name=self.region_name, signature_version="v4", retries={"max_attempts": 10, "mode": "standard"}) + + self._boto3_session = boto3.Session( + # aws_access_key_id=self.aws_access_key_id, + # aws_secret_access_key=self.aws_access_key_secret, + region_name=self.region_name + ) + + if DEBUG_LEVEL == "debug": + boto3.set_stream_logger("", level=logging.DEBUG) + else: + boto3.set_stream_logger("", level=logging.INFO) + + def _initiate_clients(self): + if get_var("AWS_DISABLED", "").lower() == "true": + logger.info("The variable AWS_DISABLED is present, not initiating boto3") + return + + if not get_var("AWS_ENABLED"): + logger.error("The variable AWS_ENABLED is not present, not initiating boto3") + return + + self._initiate_session() + + try: + if not self._boto3_session.client("sts").get_caller_identity(): + logger.info("No AWS Credentials found, not initiating clients.") + return + except (NoCredentialsError, PartialCredentialsError) as error: + logger.error(error) + return None + + self._schedule_client = self._boto3_session.client("scheduler") + self.schedule_client = self._schedule_client + self._dynamodb_client = self._boto3_session.client("dynamodb") + self.dynamodb_client = self._dynamodb_client + + self.SCHEDULE_EXCEPTIONS = self._schedule_client.exceptions + self.DYNAMO_EXCEPTIONS = self._dynamodb_client.exceptions + self.initiated = True + + logger.info("Boto3Handler has been initiated!") + + +BOTO3_HANDLER = Boto3Handler() diff --git a/src/core/config.py b/src/core/config.py new file mode 100644 index 0000000..c2e5710 --- /dev/null +++ b/src/core/config.py @@ -0,0 +1,31 @@ +from copy import copy + + +class CoreConfig: + core_defaults = { + 'CORE_BILLING_ENABLED': False, + 'CORE_EXPIRY_MODELS': ["core.TeamInvitation", "core.PasswordSecret"] + } + + django_defaults = { + 'DEFAULT_AUTO_FIELD': 'django.db.models.BigAutoField', + 'SOCIAL_AUTH_USER_MODEL': "core.User", + 'AUTH_USER_MODEL': "core.User", + } + + def _setup(self): + from django.conf import settings + + options = {option: getattr(settings, option) for option in dir(settings) if option.startswith('CORE')} + options.update(self.django_defaults) + self.attrs = copy(self.core_defaults) + self.attrs.update(options) + + def __init__(self): + self._setup() + + def __getattr__(self, item): + return self.attrs.get(item, None) + + def __setattribute__(self, key, value): + self.attrs[key] = value diff --git a/src/core/data/__init__.py b/src/core/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/data/default_email_templates.py b/src/core/data/default_email_templates.py new file mode 100644 index 0000000..511ab10 --- /dev/null +++ b/src/core/data/default_email_templates.py @@ -0,0 +1,71 @@ +from textwrap import dedent + + +def recurring_invoices_invoice_created_default_email_template() -> str: + return dedent( + """ + Hi $first_name, + + Your invoice #$invoice_id is now available and is due by $due_date. Please make your payment at your earliest convenience. + + Balance Due: $currency_symbol$amount_due $currency + View or Pay Online: $invoice_link + If you are paying by standing order, no further action is required. Should you have any questions or concerns, feel free to reach out to us. + + Thank you for your prompt attention to this matter. + + Best regards, + $company_name + """ + ).strip() + + +def recurring_invoices_invoice_overdue_default_email_template() -> str: + return dedent( + """ + Hi $first_name, + + We wanted to remind you that invoice #$invoice_id is now overdue. Please arrange payment as soon as possible to ensure there’s no interruption in your service. If you’ve already made the payment, kindly disregard this message—our apologies for any confusion. + + Here are the details for your convenience: + + Balance Due: $currency_symbol$amount_due $currency + Due Date: $due_date + + If you have any questions or concerns, we’re happy to help. Please don’t hesitate to reach out. + + Thank you for your prompt attention to this matter. + + Warm regards, + $company_name + """ + ).strip() + + +def recurring_invoices_invoice_cancelled_default_email_template() -> str: + return dedent( + """ + Hi $first_name, + + The invoice #$invoice_id has been cancelled. You do not have to pay the invoice. + + If you have any questions or concerns, please feel free to contact us. + + Many thanks, + $company_name + """ + ).strip() + + +def email_footer() -> str: + return ( + "\n\n" + + dedent( + """ + Note: This is an automated email sent by MyFinances on behalf of '$company_name'. + + If you believe this email is spam or fraudulent, please do not pay the invoice and report it to us immediately at report@myfinances.cloud. + Once reported, we will open a case for investigation. In some cases, eligible reports may qualify for a reward, determined on a case-by-case basis. + """ + ).strip() + ) diff --git a/src/core/data/default_feature_flags.py b/src/core/data/default_feature_flags.py new file mode 100644 index 0000000..fa59053 --- /dev/null +++ b/src/core/data/default_feature_flags.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class FeatureFlag: + name: str + description: str + default: bool + + +default_feature_flags: list[FeatureFlag] = [ + FeatureFlag(name="areSignupsEnabled", description="Are new account creations allowed", default=True), + FeatureFlag( + name="isInvoiceSchedulingEnabled", + description="Invoice Scheduling allows for clients to create invoice schedules that send and invoice at a specific date.", + default=False, + ), + FeatureFlag(name="areUserEmailsAllowed", description="Are users allowed to send emails from YOUR DOMAIN to customers", default=False), + FeatureFlag( + name="areInvoiceRemindersEnabled", + description="Invoice Reminders allow for clients to be reminded to pay an invoice.", + default=False, + ), +] diff --git a/src/core/data/default_quota_limits.py b/src/core/data/default_quota_limits.py new file mode 100644 index 0000000..2e1791f --- /dev/null +++ b/src/core/data/default_quota_limits.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal + + +@dataclass +class QuotaItem: + slug: str + name: str + description: str + default_value: int + adjustable: bool + period: Literal[ + "forever", + "per_month", + "per_minute", + "per_hour", + "per_day", + "per_client", + "per_invoice", + "per_team", + "per_quota", + "per_bulk_send", + "per_email", + ] + + +@dataclass +class QuotaGroup: + name: str + items: list[QuotaItem] + + +default_quota_limits: list[QuotaGroup] = [ + QuotaGroup( + "invoices", + [ + QuotaItem( + slug="count", + name="Creations per month", + description="Amount of invoices created per month", + default_value=100, + period="per_month", + adjustable=True, + ), + QuotaItem( + slug="schedules", + name="Schedules per month", + description="Amount of invoice scheduled sends allowed per month", + default_value=100, + period="per_month", + adjustable=True, + ), + QuotaItem( + slug="access_codes", + name="Created access codes", + description="Amount of invoice access codes allowed per invoice", + default_value=3, + period="per_invoice", + adjustable=True, + ), + ], + ), + QuotaGroup( + "receipts", + [ + QuotaItem( + slug="count", + name="Created receipts", + description="Amount of receipts stored per month", + default_value=100, + period="per_month", + adjustable=True, + ) + ], + ), + QuotaGroup( + "clients", + [ + QuotaItem( + slug="count", + name="Created clients", + description="Amount of clients stored in total", + default_value=40, + period="forever", + adjustable=True, + ) + ], + ), + QuotaGroup( + "teams", + [ + QuotaItem( + slug="count", + name="Created teams", + description="Amount of teams created in total", + default_value=3, + period="forever", + adjustable=True, + ), + QuotaItem( + slug="joined", + name="Joined teams", + description="Amount of teams that you have joined in total", + default_value=5, + period="forever", + adjustable=True, + ), + QuotaItem( + slug="user_count", + name="Users per team", + description="Amount of users per team", + default_value=10, + period="per_team", + adjustable=True, + ), + ], + ), + QuotaGroup( + "quota_increase", + [ + QuotaItem( + slug="request", + name="Quota Increase Request", + description="Amount of increase requests allowed per quota", + default_value=1, + period="per_quota", + adjustable=False, + ), + QuotaItem( + slug="requests_per_month_per_quota", + name="Quota Increase Requests per month", + description="Amount of increase requests allowed per month per quota", + period="per_quota", + default_value=1, + adjustable=False, + ), + ], + ), + QuotaGroup( + "emails", + [ + QuotaItem( + slug="single-count", + name="Single Email Sends", + description="Amount of single email sends allowed per month", + period="per_month", + default_value=10, + adjustable=True, + ), + QuotaItem( + slug="bulk-count", + name="Bulk Email Sends", + description="Amount of 'Bulk Emails' allowed to be sent per month", + period="per_month", + default_value=1, + adjustable=True, + ), + QuotaItem( + slug="bulk-max_sends", + name="Bulk Email Maximum Emails", + description="Maximum amount of emails allowed to be sent per 'Bulk' request", + period="per_bulk_send", + default_value=10, + adjustable=True, + ), + QuotaItem( + slug="email_character_count", + name="Maximum Character Count", + description="Maximum amount of characters allowed in an email", + period="per_email", + default_value=1000, + adjustable=True, + ), + QuotaItem( + slug="complaints", + name="Complaints allowed", + description="Maximum amount of complaints allowed before your account will be blocked from sending emails", + period="forever", + default_value=2, + adjustable=True, + ), + ], + ), +] diff --git a/src/core/decorators.py b/src/core/decorators.py new file mode 100644 index 0000000..670e58c --- /dev/null +++ b/src/core/decorators.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +import logging +from functools import wraps +from typing import TypedDict + +from django.contrib import messages +from django.http import HttpResponse +from django.http import HttpResponseRedirect +from django.shortcuts import redirect +from django.shortcuts import render +from django.urls import reverse + +from core.models import TeamMemberPermission +from core.types.requests import WebRequest +from core.utils.feature_flags import get_feature_status + +logger = logging.getLogger(__name__) + + +def not_authenticated(view_func): + def wrapper_func(request, *args, **kwargs): + if request.user.is_authenticated: + return redirect("dashboard") + else: + return view_func(request, *args, **kwargs) + + return wrapper_func + + +def staff_only(view_func): + def wrapper_func(request, *args, **kwargs): + if request.user.is_staff and request.user.is_authenticated: + return view_func(request, *args, **kwargs) + else: + messages.error(request, "You don't have permission to view this page.") + return redirect("dashboard") + + return wrapper_func + + +def superuser_only(view_func): + def wrapper_func(request, *args, **kwargs): + if request.user.is_authenticated and request.user.is_superuser: + return view_func(request, *args, **kwargs) + else: + messages.error(request, "You don't have permission to view this page.") + return redirect("dashboard") + + return wrapper_func + + +def htmx_only(viewname: str = "dashboard"): + def decorator(view_func): + def wrapper_func(request, *args, **kwargs): + if request.htmx: + return view_func(request, *args, **kwargs) + else: + return redirect(viewname) + + return wrapper_func + + return decorator + + +def hx_boost(view): + """ + Decorator for HTMX requests. + + used by wrapping FBV in @hx_boost and adding **kwargs to param + then you can use context = kwargs.get("context", {}) to continue and then it will handle HTMX boosts + """ + + @wraps(view) + def wrapper(request, *args, **kwargs): + if request.htmx.boosted: + kwargs["context"] = kwargs.get("context", {}) | {"base": "core/base/htmx.html"} + return view(request, *args, **kwargs) + + return wrapper + + +def feature_flag_check(flag, status=True, api=False, htmx=False): + def decorator(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + feat_status = get_feature_status(flag) + + if feat_status == status: + return view_func(request, *args, **kwargs) + + if api and htmx: + messages.error(request, "This feature is currently disabled.") + return render(request, "core/base/toasts.html") + elif api: + return HttpResponse(status=403, content="This feature is currently disabled.") + messages.error(request, "This feature is currently disabled.") + try: + last_visited_url = request.session["last_visited"] + current_url = request.build_absolute_uri() + if last_visited_url != current_url: + return HttpResponseRedirect(last_visited_url) + except KeyError: + pass + return HttpResponseRedirect(reverse("dashboard")) + + return wrapper + + return decorator + + +class FlagItem(TypedDict): + name: str + desired: bool + + +def feature_flag_check_multi(flag_list: list[FlagItem], api=False, htmx=False): + """ + Checks if at least one of the flags in the list is the desired status + """ + + def decorator(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + if not any(get_feature_status(flag["name"]) == flag["desired"] for flag in flag_list): + if api and htmx: + messages.error(request, "This feature is currently disabled.") + return render(request, "core/base/toasts.html") + elif api: + return HttpResponse(status=403, content="This feature is currently disabled.") + messages.error(request, "This feature is currently disabled.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + return view_func(request, *args, **kwargs) + + return wrapper + + return decorator + + +# def quota_usage_check(limit: str | QuotaLimit, extra_data: str | int | None = None, api=False, htmx=False): +# def decorator(view_func): +# @wraps(view_func) +# def wrapper(request, *args, **kwargs): +# try: +# quota_limit = QuotaLimit.objects.get(slug=limit) if isinstance(limit, str) else limit +# except QuotaLimit.DoesNotExist: +# return view_func(request, *args, **kwargs) +# +# if not quota_limit.strict_goes_above_limit(request.user, extra=extra_data): +# return view_func(request, *args, **kwargs) +# +# if api and htmx: +# messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") +# return render(request, "partials/messages_list.html", {"autohide": False}) +# elif api: +# return HttpResponse(status=403, content=f"You have reached the quota limit for this service '{quota_limit.slug}'") +# messages.error(request, f"You have reached the quota limit for this service '{quota_limit.slug}'") +# try: +# last_visited_url = request.session["last_visited"] +# current_url = request.build_absolute_uri() +# if last_visited_url != current_url: +# return HttpResponseRedirect(last_visited_url) +# except KeyError: +# pass +# return HttpResponseRedirect(reverse("dashboard")) +# +# return wrapper +# +# return decorator + + +not_logged_in = not_authenticated +logged_out = not_authenticated + + +def web_require_scopes(scopes: str | list[str], htmx=False, api=False, redirect_url=None): + """ + Only to be used by WebRequests (htmx or html) NOT PUBLIC API + """ + + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(request: WebRequest, *args, **kwargs): + if request.team_id and not request.team: + return return_error(request, "Team not found") + + if request.team: + # Check for team permissions based on team_id and scopes + if not request.team.is_owner(request.user): + team_permissions = TeamMemberPermission.objects.filter(team=request.team, user=request.user).first() + + if not team_permissions: + return return_error(request, "You do not have permission to perform this action (no permissions for team)") + + # single scope + if isinstance(scopes, str) and scopes not in team_permissions.scopes: + return return_error(request, f"You do not have permission to perform this action ({scopes})") + + # scope list + if isinstance(scopes, list): + for scope in scopes: + if scope not in team_permissions.scopes: + return return_error(request, f"You do not have permission to perform this action ({scope})") + return view_func(request, *args, **kwargs) + + _wrapped_view.required_scopes = scopes + return _wrapped_view + + def return_error(request: WebRequest, msg: str): + logging.info(f"User does not have permission to perform this action (User ID: {request.user.id}, Scopes: {scopes})") + if api and htmx: + messages.error(request, msg) + return render(request, "core/base/toast.html", {"autohide": False}) + elif api: + return HttpResponse(status=403, content=msg) + elif request.htmx: + messages.error(request, msg) + resp = HttpResponse(status=200) + + try: + last_visited_url = request.session["last_visited"] + current_url = request.build_absolute_uri() + if last_visited_url != current_url: + resp["HX-Replace-Url"] = last_visited_url + except KeyError: + ... + resp["HX-Refresh"] = "true" + return resp + + messages.error(request, msg) + + try: + last_visited_url = request.session["last_visited"] + current_url = request.build_absolute_uri() + if last_visited_url != current_url: + return HttpResponseRedirect(last_visited_url) + except KeyError: + pass + + if not redirect_url: + return HttpResponseRedirect(reverse("dashboard")) + + try: + return HttpResponseRedirect(reverse(redirect_url)) + except KeyError: + return HttpResponseRedirect(reverse("dashboard")) + + return decorator + + +# wrapper around billing has_entitlements only load + +from django.conf import settings + + +def has_entitlements(entitlements: list[str] | str, htmx_api: bool = False): + def decorator(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + if settings.BILLING_ENABLED: + from billing.decorators import has_entitlements_called_from_backend_handler + + wrapped_view_func = has_entitlements_called_from_backend_handler( + entitlements if isinstance(entitlements, list) else [entitlements], htmx_api + )(view_func) + return wrapped_view_func(request, *args, **kwargs) + return view_func(request, *args, **kwargs) + + return wrapper + + return decorator diff --git a/src/core/manage.py b/src/core/manage.py new file mode 100644 index 0000000..eff7ae0 --- /dev/null +++ b/src/core/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + 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/src/core/management/__init__.py b/src/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/management/commands/__init__.py b/src/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/management/commands/auto.py b/src/core/management/commands/auto.py new file mode 100644 index 0000000..aa84391 --- /dev/null +++ b/src/core/management/commands/auto.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand +from core.service.maintenance.expire.run import expire_and_cleanup_objects + + +class Command(BaseCommand): + """ + Runs automation scripts to make sure objects are up to date, expired objects are deleted, etc. + """ + + def handle(self, *args, **kwargs): + self.stdout.write("Running expire + cleanup script...") + self.stdout.write(expire_and_cleanup_objects()) diff --git a/src/core/management/commands/contributors.json b/src/core/management/commands/contributors.json new file mode 100644 index 0000000..2699ea5 --- /dev/null +++ b/src/core/management/commands/contributors.json @@ -0,0 +1,266 @@ +[ + { + "name": "Trey", + "username": "TreyWW", + "role": "Project Lead", + "tags": [ + "👑", + "đŸ–Ĩ" + ] + }, + { + "name": "Slawek Bierwiaczonek", + "username": "Domejko", + "role": "Development & CI & Bug Fixing", + "tags": [ + "đŸ§Ē", + "đŸ–Ĩ", + "🐞" + ] + }, + { + "name": "Sergey G", + "username": "introkun", + "role": "Development & CI", + "tags": [ + "â™ģ", + "đŸ§Ē", + "đŸ–Ĩ", + "🎨" + ] + }, + { + "name": "Jacob", + "username": "Z3nKrypt", + "role": "Documentation", + "tags": [ + "📖" + ] + }, + { + "name": "Tom", + "username": "tomkinane", + "role": "Design", + "tags": [ + "🎨" + ] + }, + { + "name": "SharonAliyas5573", + "username": "SharonAliyas5573", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "romana-la", + "username": "romana-la", + "role": "Documentation", + "tags": [ + "📖" + ] + }, + { + "name": "flyingdev", + "username": "flyingdev", + "role": "CI", + "tags": [ + "đŸ§Ē" + ] + }, + { + "name": "chavi362", + "username": "chavi362", + "role": "Documentation", + "tags": [ + "📖" + ] + }, + { + "name": "bermr", + "username": "bermr", + "role": "CI", + "tags": [ + "đŸ§Ē" + ] + }, + { + "name": "PhilipZara", + "username": "PhilipZara", + "role": "Design", + "tags": [ + "🎨" + ] + }, + { + "name": "Tianrui-Luo", + "username": "Tianrui-Luo", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "HarryHuCodes", + "username": "HarryHuCodes", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "Nuova", + "username": "Nuovaxu", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "HessTaha", + "username": "HessTaha", + "role": "CI-CD", + "tags": [ + "đŸŗ" + ] + }, + { + "name": "wnm210", + "username": "wnm210", + "role": "Design", + "tags": [ + "🎨" + ] + }, + { + "name": "Matt", + "username": "matthewjuarez1", + "role": "Full Stack", + "tags": [ + "đŸ–Ĩ", + "🎨" + ] + }, + { + "name": "SBMOYO", + "username": "SBMOYO", + "role": "CI", + "tags": [ + "đŸ§Ē" + ] + }, + { + "name": "Kevin Liu", + "username": "kliu6151", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "Jehad Altoutou", + "username": "HappyLife2", + "role": "Design", + "tags": [ + "🎨" + ] + }, + { + "name": "Samuel P", + "username": "spalominor", + "role": "Design", + "tags": [ + "🎨" + ] + }, + { + "name": "Sabari Ragavendra CK", + "username": "CKsabari2001", + "role": "Layout", + "tags": [ + "🎨" + ] + }, + { + "name": "atulanand25", + "username": "atulanand25", + "role": "Full Stack", + "tags": [ + "🎨", + "đŸ–Ĩ", + "🐞" + ] + }, + { + "name": "ryansurf", + "username": "ryansurf", + "role": "Bug Fixes", + "tags": [ + "🐞" + ] + }, + { + "name": "David", + "username": "blocage", + "role": "Refactoring", + "tags": "â™ģ" + }, + { + "name": "Guillermo", + "username": "glizondo", + "role": "Bug Fixes", + "tags": [ + "🐞" + ] + }, + { + "name": "Marvin Lopez", + "username": "marvinl803", + "role": "Full Stack", + "tags": [ + "🎨", + "đŸ–Ĩ" + ] + }, + { + "name": "Artem Kolpakov", + "username": "artkolpakov", + "role": "Bug Fixes", + "tags": [ + "🐞" + ] + }, + { + "name": "Yadu", + "username": "Yadu-M", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "Vatsal", + "username": "vatsaaaal", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "Hussein", + "username": "hussein-mamane", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + }, + { + "name": "pvvramakrishnarao234", + "username": "pvvramakrishnarao234", + "role": "Development", + "tags": [ + "đŸ–Ĩ" + ] + } +] diff --git a/src/core/management/commands/contributors.py b/src/core/management/commands/contributors.py new file mode 100644 index 0000000..8c2915e --- /dev/null +++ b/src/core/management/commands/contributors.py @@ -0,0 +1,295 @@ +import json +import os +from typing import TypedDict + +from django.core.management.base import BaseCommand +from django.utils.termcolors import colorize + + +class ContributorsItem(TypedDict): + name: str + username: str + role: str + tags: list[str] + + +class Command(BaseCommand): + """ + Adds contributors HTML table to README.md file. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.script_dir = os.path.dirname(os.path.abspath(__file__)) + self.contributors_json_path = os.path.join(self.script_dir, "contributors.json") + self.readme_path = "README.md" + + def add_arguments(self, parser): + parser.add_argument("action", type=str, help="sync, list, add, edit") + + parser.add_argument("--sort", type=str, help="Sort by: name, username or role") + parser.add_argument("--limit", type=int, default=20, help="Limit results by amount") + + parser.add_argument("name", type=str, nargs="?", help="users human/readable name") + parser.add_argument("--name", type=str, help="users human/readable name") + parser.add_argument("username", type=str, nargs="?", help="github username") + parser.add_argument("--username", type=str, help="github username") + parser.add_argument("role", type=str, nargs="?", help="role in team") + parser.add_argument("--role", type=str, help="role in team") + parser.add_argument("tags", type=str, nargs="*", help="comma separated list of tags") + parser.add_argument("--tags", type=str, help="comma separated list of tags") + + def handle(self, *args, **kwargs): + action = kwargs["action"] + + if action == "sync": + self.sync_contributors() + elif action == "list": + self.list_contributors(*args, **kwargs) + elif action == "add": + self.add_contributor(*args, **kwargs) + elif action == "help": + self.stdout.write( + colorize( + """ +Please provide valid action. +- sync +- list --sort --limit +- add + to use multi-word usernames or names, surround with quotes + tags can be space separated + to change the order use --, e.g. --name bob + """, + fg="red", + opts=("bold",), + ) + ) + else: + self.stdout.write(colorize("Please provide valid action. \n - sync \n - list \n - add \n - help", fg="red", opts=("bold",))) + + def add_contributor(self, *args, **kwargs): + name: str = kwargs.get("name") + username: str = kwargs.get("username") + role: str = kwargs.get("role") + tags: list[str] = kwargs.get("tags") + + if not name or not username or not role or not tags: + return self.stdout.write(colorize("Please provide name, username, role and tags", fg="red", opts=("bold",))) + + if not all([t in ["👑", "đŸ–Ĩ", "🎨", "📖", "đŸŗ", "â™ģ", "🐞"] for t in tags]): + return self.stdout.write( + colorize(f"Please provide valid tags. Valid tags are: 👑, đŸ–Ĩ, 🎨, 📖, đŸŗ, â™ģ, 🐞", fg="red", opts=("bold",)) + ) + + contributors_data: list[ContributorsItem] = self._read_contributor_file() + + for user in contributors_data: + if user["username"] == username: + return self.stdout.write(colorize("User already exists", fg="red", opts=("bold",))) + + contributor_obj = ContributorsItem(name=name, username=username, role=role, tags=tags) + + if contributors_data: + contributors_data.append(contributor_obj) + else: + return self.stdout.write( + colorize("contributors.json file not found. Please make sure the file exists.", fg="red", opts=("bold",)) + ) + + self._save_contributors_file(contributors_data) + + def list_contributors(self, *args, **kwargs): + contributors_data: list[ContributorsItem] | None = self._read_contributor_file() + + if not contributors_data: + return + + if kwargs.get("sort") == "name": + contributors_data = sorted(contributors_data, key=lambda d: d.get("name", "")) + elif kwargs.get("sort") == "username": + contributors_data = sorted(contributors_data, key=lambda d: d.get("username", "")) + elif kwargs.get("sort") == "role": + contributors_data = sorted(contributors_data, key=lambda d: d.get("role", "")) + + if limit := kwargs.get("limit"): + bef_count = len(contributors_data) + contributors_data = contributors_data[:limit] + aft_count = len(contributors_data) + + max_w_name = max(len(d.get("name", "")) + 4 for d in contributors_data) + max_w_username = max(len(d.get("username", "")) + 4 for d in contributors_data) + max_w_role = max(len(d.get("role", "")) + 4 for d in contributors_data) + + row_str = "{:<{max_w_name}} {:<{max_w_username}} {:<{max_w_role}} {:<10}" + + header = row_str.format( + "Name", "Username", "Role", "Tags", max_w_name=max_w_name, max_w_username=max_w_username, max_w_role=max_w_role + ) + + self.stdout.write(header) + + for user in contributors_data: + row = row_str.format( + user.get("name"), + user.get("username"), + user.get("role"), + " ".join(user.get("tags")), + max_w_name=max_w_name, + max_w_username=max_w_username, + max_w_role=max_w_role, + ) + self.stdout.write(row) + + if limit: + # noinspection PyUnboundLocalVariable + self.stdout.write(f"\nShowing {aft_count} of {bef_count} contributors\n") + + def sync_contributors(self): + contributors_data = self._read_contributor_file() + + if not contributors_data: + return + + # Path to contributors.json file in the same directory as the script + + # HTML template for each contributor entry + contributor_template = """ + + + +
+ + {name} + +
+
+ {tags} + + """ + + # Function to generate HTML for a contributor + def generate_contributor_html(contributor): + tags_html = "".join( + [ + f'{tag_icon}' + for tag_icon, tag_link, tag_title in [ + ( + "👑", + f'https://github.com/TreyWW/MyFinances/pulls?q=user%3A{contributor["username"]}', + "Project Lead", + ), + ( + "đŸ–Ĩ", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Backend", + ), + ( + "📖", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Documentation", + ), + ( + "🎨", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Frontend", + ), + ( + "🐞", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Bug Fixes", + ), + ( + "đŸ§Ē", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Added Tests", + ), + ( + "đŸŗ", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Docker", + ), + ( + "â™ģ", + f'https://github.com/TreyWW/MyFinances/pulls?q=is%3Apr+author%3A{contributor["username"]}', + "Refactored Files", + ), + ] + if tag_icon in contributor.get("tags", []) + ] + ) + return contributor_template.format( + username=contributor["username"], + name=contributor["name"], + role=contributor.get("role"), + tags=tags_html, + ) + + # Generate HTML for contributors + contributors_html = "" + users_per_row = 5 + for index, contributor in enumerate(contributors_data, start=1): + if (index - 1) % users_per_row == 0 and index > 1: + contributors_html += "" + contributors_html += f"{generate_contributor_html(contributor)}" + + # Wrap the entire content in a table row + contributors_html = f"{contributors_html}" + + try: + # Read the README.md file + with open(self.readme_path, encoding="utf-8") as readme_file: + readme_content = readme_file.read() + except FileNotFoundError: + self.stderr.write(self.style.ERROR("README.md file not found. Please make sure the file exists.")) + return + + # Insert the generated HTML between the comments + start_comment = "" + end_comment = "" + start_index = readme_content.find(start_comment) + len(start_comment) + end_index = readme_content.find(end_comment) + + new_readme_content = readme_content[:start_index] + "\n\n" + contributors_html + "
\n" + readme_content[end_index:] + + self._save_readme_file(new_readme_content) + + self.stdout.write(self.style.SUCCESS("HTML table inserted into README.md successfully.")) + + def _read_contributor_file(self) -> list[ContributorsItem] | None: + try: + # Load JSON data from contributors.json file + with open(self.contributors_json_path, encoding="utf-8") as json_file: + contributors_json = json_file.read() + return json.loads(contributors_json) + except FileNotFoundError: + self.stderr.write(self.style.ERROR("contributors.json file not found. Please make sure the file exists.")) + return None + except json.JSONDecodeError: + self.stderr.write(self.style.ERROR("Error decoding JSON data from contributors.json file. Please check the file contents.")) + return None + + def _read_readme_file(self) -> str | None: + try: + # Read the README.md file + with open(self.readme_path, encoding="utf-8") as readme_file: + return readme_file.read() + except FileNotFoundError: + self.stderr.write(self.style.ERROR("README.md file not found. Please make sure the file exists.")) + return None + + def _save_contributors_file(self, contributors_data: list[ContributorsItem]): + try: + # Save JSON data to contributors.json file + with open(self.contributors_json_path, "w", encoding="utf-8") as json_file: + json.dump(contributors_data, json_file, indent=4, ensure_ascii=False) + except FileNotFoundError: + self.stderr.write(self.style.ERROR("contributors.json file not found. Please make sure the file exists.")) + return + except json.JSONDecodeError: + self.stderr.write(self.style.ERROR("Error encoding JSON data to contributors.json file. Please check the file contents.")) + return + + def _save_readme_file(self, new_readme_content): + with open(self.readme_path, "w", encoding="utf-8") as readme_file: + readme_file.write(new_readme_content) diff --git a/src/core/management/commands/feature_flags.py b/src/core/management/commands/feature_flags.py new file mode 100644 index 0000000..a793da3 --- /dev/null +++ b/src/core/management/commands/feature_flags.py @@ -0,0 +1,55 @@ +from django.core.management.base import BaseCommand +from django.db.models.functions import Length +from django.utils.termcolors import colorize +from core.models import FeatureFlags + + +class Command(BaseCommand): + help = "Manage the feature flag statuses" + + def add_arguments(self, parser): + parser.add_argument("action", type=str, help="enable, disable or list") + parser.add_argument("flag", type=str, nargs="?", help="feature flag name") + # parser.add_argument("-f", type=str, dest="flag", help="feature flag name") + + # + def handle(self, *args, **kwargs): + if kwargs["action"] == "list": + flags = FeatureFlags.objects.annotate(name_len=Length("name"), description_len=Length("description")) + width = flags.order_by("-name_len").first().name_len + 4 + description_width = flags.order_by("-description_len").first().description_len + 4 + + header = "{:<{width}} {:<10} {:<{description_width}} {:<20}".format( + "Name", "Enabled", "Description", "Last updated", width=width, description_width=description_width + ) + self.stdout.write("Feature flags:") + self.stdout.write(header) + + for flag in FeatureFlags.objects.all(): + value = "✔" if flag.value else "❌" + + formatted_date = flag.updated_at.strftime("%Y-%m-%d %H:%M:%S") + row = "{:<{width}} {:<10} {:<{description_width}} {:<20}".format( + flag.name, value, flag.description or "No description", formatted_date, width=width, description_width=description_width + ) + self.stdout.write(row) + return + + if not kwargs["flag"]: + self.stdout.write( + colorize("Please provide a feature flag name with `feature_flags enable|disable `", fg="red", opts=("bold",)) + ) + return + + try: + flag = FeatureFlags.objects.get(name=kwargs["flag"]) + + if kwargs["action"] == "enable": + flag.enable() + self.stdout.write(f"[👍] Feature flag {kwargs['flag']} has been enabled") + elif kwargs["action"] == "disable": + flag.disable() + self.stdout.write(f"[👍] Feature flag {kwargs['flag']} has been disabled") + except FeatureFlags.DoesNotExist: + self.stdout.write(colorize("Feature flag with the name of `{kwargs['flag']}` does not exist", fg="red", opts=("bold",))) + return diff --git a/src/core/management/commands/generate_aws_scheduler_apikey.py b/src/core/management/commands/generate_aws_scheduler_apikey.py new file mode 100644 index 0000000..2bea0cc --- /dev/null +++ b/src/core/management/commands/generate_aws_scheduler_apikey.py @@ -0,0 +1,32 @@ +import uuid +from django.core.management.base import BaseCommand +from core.api.public import APIAuthToken + + +class Command(BaseCommand): + """ + Generates an API key for the AWS EventBridge API. + """ + + def handle(self, *args, **kwargs): + token = APIAuthToken(service=APIAuthToken.AdministratorServiceTypes.AWS_API_DESTINATION, name=str(uuid.uuid4())) + raw_key: str = token.generate_key() + token.save() + + self.stdout.write( + f""" + NOTE: Keep this key secret. It is used to authenticate your API requests with the AWS EventBridge API. + + Your API Key: {raw_key} + + To use this API Key for development you can use: + + pulumi config set api_destination-api_key {raw_key} + pulumi up + + If you would like to use it for production use: + pulumi stack select production + pulumi config set api_destination-api_key {raw_key} + pulumi up + """ + ) diff --git a/src/core/management/commands/lint.py b/src/core/management/commands/lint.py new file mode 100644 index 0000000..cb401e7 --- /dev/null +++ b/src/core/management/commands/lint.py @@ -0,0 +1,32 @@ +import subprocess + +from django.core.management import BaseCommand +from django.utils.termcolors import colorize + + +class Command(BaseCommand): + help = "Run linters" + requires_system_checks = [] + requires_migrations_checks = False + + def add_arguments(self, parser): + parser.add_argument("action", type=str, nargs="?", help="djlint or black") # parser.add_argument("-f", type=str, dest="flag", + + def handle(self, *args, **kwargs): + if kwargs["action"] == "djlint": + djlint() + elif kwargs["action"] == "black": + black() + else: + self.stdout.write(colorize("Linting with: BLACK FORMATTER", fg="green", opts=("bold",))) + black() + self.stdout.write(colorize("Linting with: DJLINT", fg="green", opts=("bold",))) + djlint() + + +def djlint(): + subprocess.run(["djlint", "./frontend/templates/", "--reformat"]) + + +def black(): + subprocess.run(["black", "./"]) diff --git a/src/core/management/commands/list_urls.py b/src/core/management/commands/list_urls.py new file mode 100644 index 0000000..b1dbe3c --- /dev/null +++ b/src/core/management/commands/list_urls.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand +from django.urls import get_resolver + +class Command(BaseCommand): + help = 'List all URL patterns' + + def handle(self, *args, **kwargs): + url_patterns = get_resolver().url_patterns + self.stdout.write('List of URL patterns:') + for pattern in url_patterns: + self.stdout.write(str(pattern)) \ No newline at end of file diff --git a/src/core/management/commands/navbar_refresh.py b/src/core/management/commands/navbar_refresh.py new file mode 100644 index 0000000..40757f1 --- /dev/null +++ b/src/core/management/commands/navbar_refresh.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand +from django.core.cache import cache + + +class Command(BaseCommand): + """ + Deletes the "navbar_items" cache and prints a message to the standard output. + """ + + def handle(self, *args, **kwargs): + cache.delete("navbar_items") + self.stdout.write("Cleared cache\n") diff --git a/src/core/management/commands/test_urls.py b/src/core/management/commands/test_urls.py new file mode 100644 index 0000000..51c1dcc --- /dev/null +++ b/src/core/management/commands/test_urls.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand +from django.core.management import call_command + + +class Command(BaseCommand): + help = "Runs URL verification tests." + + def handle(self, *args, **options): + call_command("test", "backend.tests.urls.verify_urls") diff --git a/src/core/management/commands/test_views.py b/src/core/management/commands/test_views.py new file mode 100644 index 0000000..97b4a08 --- /dev/null +++ b/src/core/management/commands/test_views.py @@ -0,0 +1,33 @@ +import os +from django.core.management.base import BaseCommand +from django.core.management import call_command + + +class Command(BaseCommand): + help = "Runs verification tests for view files in backend/tests/views." + + def add_arguments(self, parser): + parser.add_argument( + "test_label", + nargs="?", + type=str, + help="Test label for a specific view file.", + ) + + def handle(self, *args, **options): + test_dir = "backend/tests/views" + test_label = options["test_label"] + + if test_label: + self.run_test("backend.tests.views." + test_label) + else: + for root, dirs, files in os.walk(test_dir): + for file in files: + if file.endswith(".py") and file != "__init__.py": + test_module = file.replace(".py", "") + test_label = f"MAIN.tests.views.{test_module}" + self.run_test(test_label) + + def run_test(self, test_label): + self.stdout.write(self.style.SUCCESS(f"Running tests for {test_label}")) + call_command("test", test_label) diff --git a/src/core/migrations/0001_initial.py b/src/core/migrations/0001_initial.py new file mode 100644 index 0000000..16b09cb --- /dev/null +++ b/src/core/migrations/0001_initial.py @@ -0,0 +1,783 @@ +# Generated by Django 5.1.4 on 2024-12-21 22:26 + +import core.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="FeatureFlags", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(editable=False, max_length=100, unique=True)), + ( + "description", + models.TextField( + blank=True, editable=False, max_length=500, null=True + ), + ), + ("value", models.BooleanField(default=False)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "Feature Flag", + "verbose_name_plural": "Feature Flags", + }, + ), + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "stripe_customer_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("entitlements", models.JSONField(blank=True, default=list, null=True)), + ("awaiting_email_verification", models.BooleanField(default=True)), + ("require_change_password", models.BooleanField(default=False)), + ( + "role", + models.CharField( + choices=[ + ("DEV", "Developer"), + ("STAFF", "Staff"), + ("USER", "User"), + ("TESTER", "Tester"), + ], + default="USER", + max_length=10, + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", core.models.CustomUserManager()), + ], + ), + migrations.CreateModel( + name="Error", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("error", models.CharField(max_length=250, null=True)), + ("error_code", models.CharField(max_length=100, null=True)), + ("error_colour", models.CharField(default="danger", max_length=25)), + ("date", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="LoginLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "service", + models.CharField( + choices=[("manual", "Manual"), ("magic_link", "Magic Link")], + default="manual", + max_length=14, + ), + ), + ("date", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Notification", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("message", models.CharField(max_length=100)), + ( + "action", + models.CharField( + choices=[ + ("normal", "Normal"), + ("modal", "Modal"), + ("redirect", "Redirect"), + ], + default="normal", + max_length=10, + ), + ), + ( + "action_value", + models.CharField(blank=True, max_length=100, null=True), + ), + ("extra_type", models.CharField(blank=True, max_length=100, null=True)), + ( + "extra_value", + models.CharField(blank=True, max_length=100, null=True), + ), + ("date", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_notifications", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Organization", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "stripe_customer_id", + models.CharField(blank=True, max_length=255, null=True), + ), + ("entitlements", models.JSONField(blank=True, default=list, null=True)), + ( + "leader", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="teams_leader_of", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "members", + models.ManyToManyField( + related_name="teams_joined", to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + migrations.CreateModel( + name="AuditLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("action", models.CharField(max_length=300)), + ("date", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + ], + ), + migrations.CreateModel( + name="APIAuthToken", + fields=[ + ( + "expires", + models.DateTimeField( + blank=True, + help_text="When the item will expire", + null=True, + verbose_name="Expires", + ), + ), + ("active", models.BooleanField(default=True)), + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "hashed_key", + models.CharField(max_length=128, unique=True, verbose_name="Key"), + ), + ("name", models.CharField(max_length=64, verbose_name="Key Name")), + ( + "description", + models.TextField(blank=True, null=True, verbose_name="Description"), + ), + ( + "created", + models.DateTimeField(auto_now_add=True, verbose_name="Created"), + ), + ( + "last_used", + models.DateTimeField( + blank=True, null=True, verbose_name="Last Used" + ), + ), + ( + "scopes", + models.JSONField( + default=list, + help_text="List of permitted scopes", + verbose_name="Scopes", + ), + ), + ( + "administrator_service_type", + models.CharField( + blank=True, + max_length=64, + null=True, + verbose_name="Administrator Service Type", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + ], + options={ + "verbose_name": "API Key", + "verbose_name_plural": "API Keys", + }, + ), + migrations.AddField( + model_name="user", + name="logged_in_as_team", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.organization", + ), + ), + migrations.CreateModel( + name="PasswordSecret", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "expires", + models.DateTimeField( + blank=True, + help_text="When the item will expire", + null=True, + verbose_name="Expires", + ), + ), + ("active", models.BooleanField(default=True)), + ("secret", models.TextField(max_length=300)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="password_secrets", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="TeamInvitation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "expires", + models.DateTimeField( + blank=True, + help_text="When the item will expire", + null=True, + verbose_name="Expires", + ), + ), + ("active", models.BooleanField(default=True)), + ("code", models.CharField(max_length=10)), + ( + "invited_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_invitations", + to="core.organization", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_invitations", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Team Invitation", + "verbose_name_plural": "Team Invitations", + }, + ), + migrations.CreateModel( + name="TracebackError", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("error", models.CharField(max_length=5000, null=True)), + ("date", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="UserSettings", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("dark_mode", models.BooleanField(default=True)), + ( + "currency", + models.CharField( + choices=[ + ("GBP", "British Pound Sterling"), + ("EUR", "Euro"), + ("USD", "United States Dollar"), + ("JPY", "Japanese Yen"), + ("INR", "Indian Rupee"), + ("AUD", "Australian Dollar"), + ("CAD", "Canadian Dollar"), + ], + default="GBP", + max_length=3, + ), + ), + ( + "profile_picture", + models.ImageField( + blank=True, + null=True, + storage=core.models._public_storage, + upload_to="profile_pictures/", + ), + ), + ("disabled_features", models.JSONField(default=list)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "User Settings", + "verbose_name_plural": "User Settings", + }, + ), + migrations.CreateModel( + name="VerificationCodes", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "expires", + models.DateTimeField( + blank=True, + help_text="When the item will expire", + null=True, + verbose_name="Expires", + ), + ), + ("active", models.BooleanField(default=True)), + ( + "uuid", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ( + "token", + models.TextField(default=core.models.RandomCode, editable=False), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ( + "service", + models.CharField( + choices=[ + ("create_account", "Create Account"), + ("reset_password", "Reset Password"), + ], + max_length=14, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Verification Code", + "verbose_name_plural": "Verification Codes", + }, + ), + migrations.CreateModel( + name="EmailSendStatus", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("updated_status_at", models.DateTimeField(auto_now_add=True)), + ("recipient", models.TextField()), + ( + "aws_message_id", + models.CharField( + blank=True, editable=False, max_length=100, null=True + ), + ), + ( + "status", + models.CharField( + choices=[ + ("send", "Send"), + ("reject", "Reject"), + ("bounce", "Bounce"), + ("complaint", "Complaint"), + ("delivery", "Delivery"), + ("open", "Open"), + ("click", "Click"), + ("rendering_failure", "Rendering_Failure"), + ("delivery_delay", "Delivery_Delay"), + ("subscription", "Subscription"), + ("failed_to_send", "Failed_To_Send"), + ("pending", "Pending"), + ], + max_length=20, + ), + ), + ( + "sent_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="emails_sent", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + ], + options={ + "constraints": [ + models.CheckConstraint( + condition=models.Q( + models.Q( + ("organization__isnull", False), ("user__isnull", True) + ), + models.Q( + ("organization__isnull", True), ("user__isnull", False) + ), + _connector="OR", + ), + name="core_emailsendstatus_check_user_or_organization", + ) + ], + }, + ), + migrations.CreateModel( + name="TeamMemberPermission", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "scopes", + models.JSONField( + default=list, + help_text="List of permitted scopes", + verbose_name="Scopes", + ), + ), + ( + "team", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="permissions", + to="core.organization", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="team_permissions", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "unique_together": {("team", "user")}, + }, + ), + ] diff --git a/src/core/migrations/__init__.py b/src/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/models.py b/src/core/models.py new file mode 100644 index 0000000..ee9d65d --- /dev/null +++ b/src/core/models.py @@ -0,0 +1,504 @@ +from __future__ import annotations + +import itertools +import typing +from datetime import datetime, timedelta +from typing import Literal, Union +from uuid import uuid4 + +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import AbstractUser, UserManager +from django.core.files.storage import storages, FileSystemStorage +from django.db import models +from django.db.models import Count, QuerySet +from django.utils import timezone +from django.utils.crypto import get_random_string +from storages.backends.s3 import S3Storage + +def _public_storage(): + return storages["public_media"] + + +def _private_storage() -> FileSystemStorage | S3Storage: + return storages["private_media"] + + +def RandomCode(length=6): + return get_random_string(length=length).upper() + + +def RandomAPICode(length=89): + return get_random_string(length=length).lower() + + +def upload_to_user_separate_folder(instance, filename, optional_actor=None) -> str: + instance_name = instance._meta.verbose_name.replace(" ", "-") + + print(instance, filename) + + if optional_actor: + if isinstance(optional_actor, User): + return f"{instance_name}/users/{optional_actor.id}/{filename}" + elif isinstance(optional_actor, Organization): + return f"{instance_name}/orgs/{optional_actor.id}/{filename}" + return f"{instance_name}/global/{filename}" + + if hasattr(instance, "user") and hasattr(instance.user, "id"): + return f"{instance_name}/users/{instance.user.id}/{filename}" + elif hasattr(instance, "organization") and hasattr(instance.organization, "id"): + return f"{instance_name}/orgs/{instance.organization.id}/{filename}" + return f"{instance_name}/global/{filename}" + + +def USER_OR_ORGANIZATION_CONSTRAINT(): + return models.CheckConstraint( + name=f"%(app_label)s_%(class)s_check_user_or_organization", + check=(models.Q(user__isnull=True, organization__isnull=False) | models.Q(user__isnull=False, organization__isnull=True)), + ) + + +def add_3hrs_from_now(): + return timezone.now() + timezone.timedelta(hours=3) + + +M = typing.TypeVar("M", bound=models.Model) + + +class CustomUserManager(UserManager): + def get_queryset(self): + return ( + super() + .get_queryset() + .select_related("user_profile", "logged_in_as_team") + .annotate(notification_count=(Count("user_notifications"))) + ) + + +class User(AbstractUser): + objects: CustomUserManager = CustomUserManager() # type: ignore + + logged_in_as_team = models.ForeignKey("Organization", on_delete=models.SET_NULL, null=True, blank=True) + stripe_customer_id = models.CharField(max_length=255, null=True, blank=True) + entitlements = models.JSONField(null=True, blank=True, default=list) # list of strings e.g. ["invoices"] + awaiting_email_verification = models.BooleanField(default=True) + require_change_password = models.BooleanField(default=False) # does user need to change password upon next login + + class Role(models.TextChoices): + # NAME DJANGO ADMIN NAME + DEV = "DEV", "Developer" + STAFF = "STAFF", "Staff" + USER = "USER", "User" + TESTER = "TESTER", "Tester" + + role = models.CharField(max_length=10, choices=Role.choices, default=Role.USER) + + @property + def name(self): + return self.first_name + + @property + def teams_apart_of(self): + return set(itertools.chain(self.teams_joined.all(), self.teams_leader_of.all())) + + @property + def is_org(self): + return False + + +class ActiveManager(models.Manager): + """Manager to return only active objects.""" + + def get_queryset(self): + return super().get_queryset().filter(active=True) + + +class ExpiredManager(models.Manager): + """Manager to return only expired (inactive) objects.""" + + def get_queryset(self): + now = timezone.now() + return super().get_queryset().filter(expires__isnull=False, expires__lte=now) + + +class ExpiresBase(models.Model): + """Base model for handling expiration logic.""" + + expires = models.DateTimeField("Expires", null=True, blank=True, help_text="When the item will expire") + active = models.BooleanField(default=True) + + # Default manager that returns only active items + objects = ActiveManager() + + # Custom manager to get expired/inactive objects + expired_objects = ExpiredManager() + + # Fallback All objects + all_objects = models.Manager() + + def deactivate(self) -> None: + """Manually deactivate the object.""" + self.active = False + self.save() + + def delete_if_expired_for(self, days: int = 14) -> bool: + """Delete the object if it has been expired for a certain number of days.""" + if self.expires and self.expires <= timezone.now() - timedelta(days=days): + self.delete() + return True + return False + + @property + def remaining_active_time(self): + """Return the remaining time until expiration, or None if already expired or no expiration set.""" + if not self.has_expired: + return self.expires - timezone.now() + return None + + @property + def has_expired(self): + return self.expires and self.expires <= timezone.now() + + def is_active(self): + return self.active + + class Meta: + abstract = True + + +class VerificationCodes(ExpiresBase): + class ServiceTypes(models.TextChoices): + CREATE_ACCOUNT = "create_account", "Create Account" + RESET_PASSWORD = "reset_password", "Reset Password" + + uuid = models.UUIDField(default=uuid4, editable=False, unique=True) # This is the public identifier + token = models.TextField(default=RandomCode, editable=False) # This is the private token (should be hashed) + + user = models.ForeignKey(User, on_delete=models.CASCADE) + created = models.DateTimeField(auto_now_add=True) + service = models.CharField(max_length=14, choices=ServiceTypes.choices) + + def __str__(self): + return self.user.username + + def hash_token(self): + self.token = make_password(self.token) + self.save() + return True + + class Meta: + verbose_name = "Verification Code" + verbose_name_plural = "Verification Codes" + + +class UserSettings(models.Model): + class CoreFeatures(models.TextChoices): + INVOICES = "invoices", "Invoices" + RECEIPTS = "receipts", "Receipts" + EMAIL_SENDING = "email_sending", "Email Sending" + MONTHLY_REPORTS = "monthly_reports", "Monthly Reports" + + CURRENCIES = { + "GBP": {"name": "British Pound Sterling", "symbol": "ÂŖ"}, + "EUR": {"name": "Euro", "symbol": "â‚Ŧ"}, + "USD": {"name": "United States Dollar", "symbol": "$"}, + "JPY": {"name": "Japanese Yen", "symbol": "ÂĨ"}, + "INR": {"name": "Indian Rupee", "symbol": "₹"}, + "AUD": {"name": "Australian Dollar", "symbol": "$"}, + "CAD": {"name": "Canadian Dollar", "symbol": "$"}, + } + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="user_profile") + dark_mode = models.BooleanField(default=True) + currency = models.CharField( + max_length=3, + default="GBP", + choices=[(code, info["name"]) for code, info in CURRENCIES.items()], + ) + profile_picture = models.ImageField( + upload_to="profile_pictures/", + storage=_public_storage, + blank=True, + null=True, + ) + + disabled_features = models.JSONField(default=list) + + @property + def profile_picture_url(self): + if self.profile_picture and hasattr(self.profile_picture, "url"): + return self.profile_picture.url + return "" + + def get_currency_symbol(self): + return self.CURRENCIES.get(self.currency, {}).get("symbol", "$") + + def has_feature(self, feature: str) -> bool: + return feature not in self.disabled_features + + def __str__(self): + return self.user.username + + class Meta: + verbose_name = "User Settings" + verbose_name_plural = "User Settings" + + +class Organization(models.Model): + name = models.CharField(max_length=100, unique=True) + leader = models.ForeignKey(User, on_delete=models.CASCADE, related_name="teams_leader_of") + members = models.ManyToManyField(User, related_name="teams_joined") + + stripe_customer_id = models.CharField(max_length=255, null=True, blank=True) + entitlements = models.JSONField(null=True, blank=True, default=list) # list of strings e.g. ["invoices"] + + def is_owner(self, user: User) -> bool: + return self.leader == user + + def is_logged_in_as_team(self, request) -> bool: + if isinstance(request.auth, User): + return False + + if request.auth and request.auth.organization_id == self.id: + return True + return False + + @property + def is_authenticated(self): + return True + + @property + def is_org(self): + return True + + +class TeamMemberPermission(models.Model): + team = models.ForeignKey("core.Organization", on_delete=models.CASCADE, related_name="permissions") + user = models.OneToOneField("core.User", on_delete=models.CASCADE, related_name="team_permissions") + scopes = models.JSONField("Scopes", default=list, help_text="List of permitted scopes") + + class Meta: + unique_together = ("team", "user") + + +class TeamInvitation(ExpiresBase): + code = models.CharField(max_length=10) + team = models.ForeignKey("core.Organization", on_delete=models.CASCADE, related_name="team_invitations") + user = models.ForeignKey("core.User", on_delete=models.CASCADE, related_name="team_invitations") + invited_by = models.ForeignKey("core.User", on_delete=models.CASCADE) + + def is_active(self): + return self.active + + def set_expires(self): + self.expires = timezone.now() + timezone.timedelta(days=14) + + def save(self, *args, **kwargs): + if not self.code: + self.code = RandomCode(10) + self.set_expires() + super().save() + + def __str__(self): + return self.team.name + + class Meta: + verbose_name = "Team Invitation" + verbose_name_plural = "Team Invitations" + + +class OwnerBaseManager(models.Manager): + def create(self, **kwargs): + # Handle the 'owner' argument dynamically in `create()` + owner = kwargs.pop("owner", None) + if isinstance(owner, User): + kwargs["user"] = owner + kwargs["organization"] = None + elif isinstance(owner, Organization): + kwargs["organization"] = owner + kwargs["user"] = None + return super().create(**kwargs) + + def filter(self, *args, **kwargs): + # Handle the 'owner' argument dynamically in `filter()` + owner = kwargs.pop("owner", None) + if isinstance(owner, User): + kwargs["user"] = owner + elif isinstance(owner, Organization): + kwargs["organization"] = owner + return super().filter(*args, **kwargs) + + +class OwnerBase(models.Model): + user = models.ForeignKey("core.User", on_delete=models.CASCADE, null=True, blank=True) + organization = models.ForeignKey("core.Organization", on_delete=models.CASCADE, null=True, blank=True) + + objects = OwnerBaseManager() + + class Meta: + abstract = True + constraints = [ + USER_OR_ORGANIZATION_CONSTRAINT(), + ] + + @property + def owner(self) -> User | Organization: + """ + Property to dynamically get the owner (either User or Team) + """ + if hasattr(self, "user") and self.user: + return self.user + elif hasattr(self, "team") and self.team: + return self.team + return self.organization # type: ignore[return-value] + # all responses WILL have either a user or org so this will handle all + + @owner.setter + def owner(self, value: User | Organization) -> None: + if isinstance(value, User): + self.user = value + self.organization = None + elif isinstance(value, Organization): + self.user = None + self.organization = value + else: + raise ValueError("Owner must be either a User or a Organization") + + def save(self, *args, **kwargs): + if hasattr(self, "owner") and not self.user and not self.organization: + if isinstance(self.owner, User): + self.user = self.owner + elif isinstance(self.owner, Organization): + self.organization = self.owner + super().save(*args, **kwargs) + + @classmethod + def filter_by_owner(cls: typing.Type[M], owner: Union[User, Organization]) -> QuerySet[M]: + """ + Class method to filter objects by owner (either User or Organization) + """ + if isinstance(owner, User): + return cls.objects.filter(user=owner) # type: ignore[attr-defined] + elif isinstance(owner, Organization): + return cls.objects.filter(organization=owner) # type: ignore[attr-defined] + else: + raise ValueError("Owner must be either a User or an Organization") + + @property + def is_team(self): + return isinstance(self.owner, Organization) + + +class PasswordSecret(ExpiresBase): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="password_secrets") + secret = models.TextField(max_length=300) + + +class Notification(models.Model): + action_choices = [ + ("normal", "Normal"), + ("modal", "Modal"), + ("redirect", "Redirect"), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="user_notifications") + message = models.CharField(max_length=100) + action = models.CharField(max_length=10, choices=action_choices, default="normal") + action_value = models.CharField(max_length=100, null=True, blank=True) + extra_type = models.CharField(max_length=100, null=True, blank=True) + extra_value = models.CharField(max_length=100, null=True, blank=True) + date = models.DateTimeField(auto_now_add=True) + + +class AuditLog(OwnerBase): + action = models.CharField(max_length=300) + date = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints: list = [] + + def __str__(self): + return f"{self.action} - {self.date}" + + +class LoginLog(models.Model): + class ServiceTypes(models.TextChoices): + MANUAL = "manual" + MAGIC_LINK = "magic_link" + + user = models.ForeignKey(User, on_delete=models.CASCADE) + service = models.CharField(max_length=14, choices=ServiceTypes.choices, default="manual") + date = models.DateTimeField(auto_now_add=True) + + +class Error(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + error = models.CharField(max_length=250, null=True) + error_code = models.CharField(max_length=100, null=True) + error_colour = models.CharField(max_length=25, default="danger") + date = models.DateTimeField(auto_now=True) + + def __str__(self): + return str(self.user_id) + + +class TracebackError(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True) + error = models.CharField(max_length=5000, null=True) + date = models.DateTimeField(auto_now=True) + + def __str__(self): + return str(self.error) + + +class FeatureFlags(models.Model): + name = models.CharField(max_length=100, editable=False, unique=True) + description = models.TextField(max_length=500, null=True, blank=True, editable=False) + value = models.BooleanField(default=False) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Feature Flag" + verbose_name_plural = "Feature Flags" + + def __str__(self): + return self.name + + def enable(self): + self.value = True + self.save() + + def disable(self): + self.value = False + self.save() + + +class EmailSendStatus(OwnerBase): + STATUS_CHOICES = [ + (status, status.title()) + for status in [ + "send", + "reject", + "bounce", + "complaint", + "delivery", + "open", + "click", + "rendering_failure", + "delivery_delay", + "subscription", + "failed_to_send", + "pending", + ] + ] + + sent_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="emails_sent") + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + updated_status_at = models.DateTimeField(auto_now_add=True) + + recipient = models.TextField() + aws_message_id = models.CharField(max_length=100, null=True, blank=True, editable=False) + status = models.CharField(max_length=20, choices=STATUS_CHOICES) + + class Meta: + constraints = [USER_OR_ORGANIZATION_CONSTRAINT()] diff --git a/src/core/service/__init__.py b/src/core/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/api_keys/__init__.py b/src/core/service/api_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/api_keys/delete.py b/src/core/service/api_keys/delete.py new file mode 100644 index 0000000..ac79881 --- /dev/null +++ b/src/core/service/api_keys/delete.py @@ -0,0 +1,18 @@ +from core.models import User, Organization +from core.service.api_keys.get import get_api_key_by_name +from core.api.public import APIAuthToken + + +def delete_api_key(request, owner: User | Organization, key: str | None | APIAuthToken) -> bool | str: + if isinstance(owner, Organization) and "api_keys:write" not in owner.permissions.get(user=request.user).scopes: + return "No permission to delete key" + + if not isinstance(key, APIAuthToken): + key: APIAuthToken | None = get_api_key_by_name(owner, key) # type: ignore[no-redef, arg-type] + + if not key: + return "Key not found" + + key.deactivate() # type: ignore[union-attr] + + return True diff --git a/src/core/service/api_keys/generate.py b/src/core/service/api_keys/generate.py new file mode 100644 index 0000000..45bcaf2 --- /dev/null +++ b/src/core/service/api_keys/generate.py @@ -0,0 +1,103 @@ +from django.core.exceptions import ValidationError + +from core.api.public import APIAuthToken +from core.models import User, Organization +from core.service.permissions.scopes import validate_scopes + + +def generate_public_api_key( + request, + owner: User | Organization, + api_key_name: str | None, + permissions: list, + *, + expires=None, + description=None, + administrator_toggle: bool = False, + administrator_type: str | None = None, +) -> tuple[APIAuthToken | None, str]: + if not validate_name(api_key_name): + return None, "Invalid key name" + + if not validate_description(description): + return None, "Invalid description" + + if api_key_exists_under_name(owner, api_key_name): + return None, "A key with this name already exists in your account" + + if validate_scopes(permissions).failed: # or not has_permission_to_create(request, owner): + return None, "Invalid permissions" + + administrator_service_type = None + + if request.user.is_superuser: + if administrator_toggle: + if administrator_type not in [option[0] for option in APIAuthToken.AdministratorServiceTypes.choices]: + return None, "Invalid administration type" + administrator_service_type = administrator_type + + token = APIAuthToken( + name=api_key_name, + description=description, + expires=expires, + scopes=permissions, + administrator_service_type=administrator_service_type, + ) # type: ignore[arg-type, misc] + + raw_key: str = token.generate_key() + + if isinstance(owner, Organization): + token.organization = owner + else: + token.user = owner + + try: + token.full_clean() + except ValidationError as validation_errors: + field, error_list = next(iter(validation_errors.error_dict.items())) + + field = "Permissions" if field == "scopes" else field.title() + + if isinstance(error_list[0], ValidationError): + error_message = error_list[0].messages[0] + else: + error_message = error_list[0] + + return None, f"{field}: {error_message}" + + token.save() + + return token, raw_key + + +def validate_name(name: str | None) -> bool: + """ + Require name not already exist under account + """ + if not name: + return False + return len(name) <= 64 + + +def validate_description(description: str | None) -> bool: + """ + Accept any description + Reject description longer than 255 characters + """ + return not description or len(description) <= 255 + + +def api_key_exists_under_name(owner: User | Organization, name: str | None) -> bool: + """ + Check if API key exists under a given name + """ + return APIAuthToken.filter_by_owner(owner).filter(name=name, active=True).exists() + + +def has_permission_to_create(request, owner: User | Organization) -> bool: + if isinstance(owner, User): + return True + + if owner.permissions.filter(user=request.user).exists() and "api_keys:write" in owner.permissions.get(user=request.user).scopes: + return True + return False diff --git a/src/core/service/api_keys/get.py b/src/core/service/api_keys/get.py new file mode 100644 index 0000000..d4dfd31 --- /dev/null +++ b/src/core/service/api_keys/get.py @@ -0,0 +1,10 @@ +from core.api.public import APIAuthToken +from core.models import User, Organization + + +def get_api_key_by_name(owner: User | Organization, key_name: str) -> APIAuthToken | None: + return APIAuthToken.filter_by_owner(owner).filter(name=key_name, active=True).first() + + +def get_api_key_by_id(owner: User | Organization, key_id: str | int) -> APIAuthToken | None: + return APIAuthToken.filter_by_owner(owner).filter(id=key_id, active=True).first() diff --git a/src/core/service/asyn_tasks/__init__.py b/src/core/service/asyn_tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/asyn_tasks/tasks.py b/src/core/service/asyn_tasks/tasks.py new file mode 100644 index 0000000..c8eed48 --- /dev/null +++ b/src/core/service/asyn_tasks/tasks.py @@ -0,0 +1,95 @@ +import os +import json +import uuid +import threading +import boto3 +import inspect + +from django.urls import reverse + + +class Task: + def __init__(self, queue_url=None): + self.queue_url: str | None = queue_url or os.environ.get("AWS_SQS_QUEUE_URL") + self.region_name = os.environ.get("AWS_REGION_NAME") + self.aws_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID") + self.aws_secret_access_key = os.environ.get("AWS_ACCESS_KEY") + self.WEBHOOK_URL = os.environ.get("SITE_URL", default="http://127.0.0.1:8000") + reverse("api:public:webhooks:receive_global") + + if self.queue_url: + self.sqs_client = boto3.client( + "sqs", + # aws_access_key_id=self.aws_access_key_id, + # aws_secret_access_key=self.aws_secret_access_key, + region_name=self.region_name, + ) + else: + self.sqs_client = None # Only set up SQS client if queue_url is provided + + def queue_task(self, func, *args, **kwargs): + # Determine if func is a string or callable + if isinstance(func, str): + func_name = func + elif callable(func): + func_name = self._get_function_path(func) + else: + raise ValueError("func must be a callable or a string representing the function name.") + + # If SQS is not configured, execute the function directly + if not self.queue_url or not self.region_name: + return self.execute_now(func_name, *args, **kwargs) + + print("TASK 3 - call _send_message to SQS") + # Use threading to send the message to SQS + thread = threading.Thread(target=self._send_message, args=(func_name, args, kwargs)) + thread.start() + + return "Task submitted to SQS" + + def _send_message(self, func_name, args, kwargs): + message_body = {"func_name": func_name, "args": args, "kwargs": kwargs, "webhook_url": self.WEBHOOK_URL} + print(message_body) + + try: + print("TASK 4 - Send SQS message") + res = self.sqs_client.send_message( + QueueUrl=self.queue_url, + MessageBody=json.dumps(message_body), + MessageGroupId="default", + MessageDeduplicationId=str(uuid.uuid4()), + ) + print(f"SQS Response: {res}") + except Exception as e: + print(f"Error sending message to SQS: {str(e)}") + return self.execute_now(func_name, *args, **kwargs) + + def execute_now(self, func_name, *args, **kwargs): + print("TASK 6 - Execute function directly") + if "." not in func_name: + raise ValueError(f"Invalid function name: {func_name}. Must include module path.") + + func = globals().get(func_name) + if func is None: + try: + module_name, func_name = func_name.rsplit(".", 1) + module = __import__(module_name, fromlist=[func_name]) + func = getattr(module, func_name) + except (ImportError, AttributeError) as e: + raise ValueError(f"Function {func_name} not found: {str(e)}") + + if callable(func): + return func(*args, **kwargs) + else: + raise ValueError(f"Function {func_name} is not callable") + + def _get_function_path(self, func): + """Returns the full path of the function as a string.""" + module = inspect.getmodule(func).__name__ + func_name = func.__name__ # Use __name__ instead of __qualname__ + return f"{module}.{func_name}" + + +# Usage Example +# task_instance = Task() +# result = task_instance.queue_task('my_module.my_function', arg1, arg2) # Using string +# result = task_instance.queue_task(my_function, arg1, arg2) # Using callable diff --git a/src/core/service/base/__init__.py b/src/core/service/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/base/breadcrumbs.py b/src/core/service/base/breadcrumbs.py new file mode 100644 index 0000000..8e2c9c2 --- /dev/null +++ b/src/core/service/base/breadcrumbs.py @@ -0,0 +1,85 @@ +from typing import Optional, Any + +from django.http import HttpRequest +from django.urls import resolve, reverse +from django.urls.exceptions import NoReverseMatch + +ALL_ITEMS: dict[str, tuple[str, Optional[str], Optional[str]]] = { + "dashboard": ("Dashboard", "dashboard", "house"), + "finance:invoices:dashboard": ("Invoices", "finance:invoices:single:dashboard", "file-invoice"), + "finance:invoices:single:dashboard": ("Single", "finance:invoices:single:dashboard", "file-invoice"), + "finance:invoices:single:create": ("Create (single)", "finance:invoices:single:create", None), + "finance:invoices:recurring:dashboard": ("Recurring", "finance:invoices:recurring:dashboard", "refresh"), + "finance:invoices:recurring:create": ("Create (recurring)", "finance:invoices:recurring:create", None), + "finance:invoices:single:edit": ("Edit", None, "pencil"), + "finance:invoices:single:overview": ("Invoice", None, None), + "receipts dashboard": ("Receipts", "receipts dashboard", "file-invoice"), + "core:teams:dashboard": ("Teams", "core:teams:dashboard", "users"), + "settings:dashboard": ("Settings", "settings:dashboard", "gear"), + "clients:dashboard": ("Clients", "clients:dashboard", "users"), + "clients:create": ("Create", "clients:create", None), + "reports:dashboard": ("Monthly Reports", "reports:dashboard", "chart-line"), +} + +ALL_BREADCRUMBS: dict[str, str | tuple] = { + "dashboard": "dashboard", + "core:teams:dashboard": ("dashboard", "core:teams:dashboard"), + "receipts dashboard": ("dashboard", "receipts dashboard"), + "finance:invoices:single:dashboard": ("dashboard", "finance:invoices:dashboard", "finance:invoices:single:dashboard"), + "finance:invoices:single:create": ("dashboard", "finance:invoices:dashboard", "finance:invoices:single:create"), + "finance:invoices:recurring:dashboard": ("dashboard", "finance:invoices:dashboard", "finance:invoices:recurring:dashboard"), + "finance:invoices:recurring:create": ("dashboard", "finance:invoices:dashboard", "finance:invoices:recurring:create"), + "finance:invoices:single:edit": ("dashboard", "finance:invoices:dashboard", "finance:invoices:single:edit"), + "finance:invoices:single:overview": ("dashboard", "finance:invoices:dashboard", "finance:invoices:single:overview"), + "clients:dashboard": ("dashboard", "clients:dashboard"), + "clients:create": ("dashboard", "clients:dashboard", "clients:create"), + "settings:dashboard": ("dashboard", "settings:dashboard"), + "reports:dashboard": ("dashboard", "reports:dashboard"), +} + + +def get_item(name: str, url_name: Optional[str] = None, icon: Optional[str] = None, kwargs: dict = {}, *, request=None) -> dict: + """ + Create a breadcrumb item dictionary. + Parameters: + - name (str): The name of the breadcrumb item. + - url_name (str): The URL name used for generating the URL using Django's reverse function. + - icon (Optional[str]): The icon associated with the breadcrumb item (default is None). + Returns: + Dict[str, Any]: A dictionary representing the breadcrumb item. + """ + + if request: + rev_kwargs = {kwarg: request.resolver_match.kwargs.get(kwarg) for url, kwarg in kwargs.items() if url == url_name if kwargs} + else: + rev_kwargs = {} + return { + "name": name, + "url": reverse(url_name, kwargs=rev_kwargs if rev_kwargs else {}) if url_name else "", + "icon": icon, + } + + +def generate_breadcrumbs(*breadcrumb_list: str, request=None) -> list[dict[Any, Any] | None]: + """ + Generate a list of breadcrumb items based on the provided list of breadcrumb names. + Parameters: + - breadcrumb_list (str): Variable number of strings representing the names of the breadcrumbs. + Returns: + List[Dict[str, Any]]: A list of dictionaries representing the breadcrumb items. + """ + return [ + get_item(*ALL_ITEMS.get(breadcrumb, (None, None, None)), request=request) + for breadcrumb in breadcrumb_list + if breadcrumb in ALL_ITEMS + ] + + +def get_breadcrumbs(*, request: HttpRequest | None = None, url: str | None = None): + current_url_name: str | Any = request.resolver_match.view_name if request and request.resolver_match else None # type: ignore[ union-attr] + if url: + try: + current_url_name = resolve(url).view_name + except NoReverseMatch: + return {"breadcrumb": []} + return {"breadcrumb": generate_breadcrumbs(*ALL_BREADCRUMBS.get(current_url_name, []), request=request)} diff --git a/src/core/service/boto3/__init__.py b/src/core/service/boto3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/boto3/handler.py b/src/core/service/boto3/handler.py new file mode 100644 index 0000000..67ebf64 --- /dev/null +++ b/src/core/service/boto3/handler.py @@ -0,0 +1,84 @@ +import botocore.client +from botocore.config import Config +from botocore.exceptions import NoCredentialsError, PartialCredentialsError + +from core.utils.settings_helper import get_var + +import boto3 +import logging + +logger = logging.getLogger(__name__) + +DEBUG_LEVEL = get_var("AWS_LOG_LEVEL", default="debug") +DEBUG_LEVEL = "debug" if DEBUG_LEVEL == "debug" else "info" if DEBUG_LEVEL == "info" else None + + +class Boto3Handler: + def __init__(self): + self.initiated: bool = False + self.region_name: str = get_var("AWS_REGION_NAME", "eu-west-2") + self.aws_access_key_id: str = get_var("AWS_ACCESS_KEY_ID") + self.aws_access_key_secret: str = get_var("AWS_ACCESS_KEY") + self.scheduler_lambda_arn: str = get_var("SCHEDULER_LAMBDA_ARN") + self.scheduler_lambda_access_role_arn: str = get_var("SCHEDULER_LAMBDA_ACCESS_ROLE_ARN") + self.scheduler_invoices_group_name: str = get_var("SCHEDULER_INVOICES_GROUP_NAME") + self.dynamodb_client = None + self.scheduler_client = None + + print(f"Region: {self.region_name}") + print("| has aws access key id" if self.aws_access_key_id else "X no aws access key id") + print("| has aws access key secret" if self.aws_access_key_secret else "X no aws access key secret") + print("| has scheduler lambda arn" if self.scheduler_lambda_arn else "X no scheduler lambda arn") + print( + "| has scheduler lambda access role arn" if self.scheduler_lambda_access_role_arn else "X no scheduler lambda access role arn" + ) + print("| has scheduler invoices group name" if self.scheduler_invoices_group_name else "X no scheduler invoices group name") + + self._initiate_clients() + + def _initiate_session(self): + self._boto3_config = Config(region_name=self.region_name, signature_version="v4", retries={"max_attempts": 10, "mode": "standard"}) + + self._boto3_session = boto3.Session( + # aws_access_key_id=self.aws_access_key_id, + # aws_secret_access_key=self.aws_access_key_secret, + region_name=self.region_name + ) + + if DEBUG_LEVEL == "debug": + boto3.set_stream_logger("", level=logging.DEBUG) + else: + boto3.set_stream_logger("", level=logging.INFO) + + def _initiate_clients(self): + if get_var("AWS_DISABLED", "").lower() == "true": + logger.info("The variable AWS_DISABLED is present, not initiating boto3") + return + + if not get_var("AWS_ENABLED"): + logger.error("The variable AWS_ENABLED is not present, not initiating boto3") + return + + self._initiate_session() + + try: + if not self._boto3_session.client("sts").get_caller_identity(): + logger.info("No AWS Credentials found, not initiating clients.") + return + except (NoCredentialsError, PartialCredentialsError) as error: + logger.error(error) + return None + + self._schedule_client = self._boto3_session.client("scheduler") + self.schedule_client = self._schedule_client + self._dynamodb_client = self._boto3_session.client("dynamodb") + self.dynamodb_client = self._dynamodb_client + + self.SCHEDULE_EXCEPTIONS = self._schedule_client.exceptions + self.DYNAMO_EXCEPTIONS = self._dynamodb_client.exceptions + self.initiated = True + + logger.info("Boto3Handler has been initiated!") + + +BOTO3_HANDLER = Boto3Handler() diff --git a/src/core/service/emails/__init__.py b/src/core/service/emails/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/maintenance/__init__.py b/src/core/service/maintenance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/maintenance/expire/__init__.py b/src/core/service/maintenance/expire/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/maintenance/expire/run.py b/src/core/service/maintenance/expire/run.py new file mode 100644 index 0000000..407905b --- /dev/null +++ b/src/core/service/maintenance/expire/run.py @@ -0,0 +1,48 @@ +from datetime import timedelta +from typing import Type, Optional, List + +from django.conf import settings +from django.db import models +from django.db.models import QuerySet + +from django.apps import apps + +from django.utils import timezone + +""" +Every model MUST have the field "expires" as: + +expires = models.DateTimeField(null=True, blank=True) +""" + + +def expire_models_task(): + return expire_and_cleanup_objects(getattr(settings, "EXPIRY_MODELS", None)) + + +def expire_and_cleanup_objects(model_list: Optional[List[str]] = None) -> str: + if model_list is None: + model_list = ["core.TeamInvitation", "core.PasswordSecret"] + + deactivated_items: int = 0 + deleted_items: int = 0 + + now = timezone.now() + + for model in model_list: + app_label, model_name = model.split(".") + + model_cls = apps.get_model(app_label=app_label, model_name=model_name) + + # Delete objects that have been inactive and expired for more than 14 days + over_14_days_expired = model_cls.all_objects.filter(expires__lte=now - timedelta(days=14)) # type: ignore[attr-defined] + deleted_items += over_14_days_expired.count() + over_14_days_expired.delete() + + # Deactivate expired items that got missed + to_deactivate: QuerySet[models.Model] = model_cls.all_objects.filter(expires__lte=now, active=True) # type: ignore[attr-defined] + + deactivated_items += to_deactivate.count() + to_deactivate.update(active=False) + + return f"Deactivated {deactivated_items} objects and deleted {deleted_items} objects." diff --git a/src/core/service/maintenance/tasks.py b/src/core/service/maintenance/tasks.py new file mode 100644 index 0000000..d645941 --- /dev/null +++ b/src/core/service/maintenance/tasks.py @@ -0,0 +1,18 @@ +from django.conf import settings + +from core.service.maintenance.expire.run import expire_models_task + +CORE_MAINTENANCE_TASKS: list[callable] = [ + expire_models_task +] + + +def get_maintenance_tasks() -> list[callable]: + return getattr(settings, "MAINTENANCE_FUNCTIONS", CORE_MAINTENANCE_TASKS) + + +def execute_maintenance_tasks(tasks: list[callable]) -> str: + output = "" + for i, task in enumerate(tasks): + output += f"\n{task()}" if i != 0 else task() + return output diff --git a/src/core/service/modals/__init__.py b/src/core/service/modals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/modals/modals.py b/src/core/service/modals/modals.py new file mode 100644 index 0000000..0b03135 --- /dev/null +++ b/src/core/service/modals/modals.py @@ -0,0 +1,106 @@ +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import render + +from core.api.public import APIAuthToken +from core.api.public.permissions import SCOPE_DESCRIPTIONS +from core.models import UserSettings, Organization +from core.service.modals.registry import Modal +from core.types.requests import WebRequest + + +# class LogoutModal(Modal): +# modal_name = 'logout' +# +# def get(self, request: WebRequest, *args, **kwargs): +# return HttpResponse("logout_modal.html") + + +class PermissionModalContext: + def get_context(self, request: WebRequest) -> dict: + # example + # "clients": { + # "description": "Access customer details", + # "options": ["read", "write"] + # } + return { + "permissions": [ + {"name": group, "description": perms["description"], "options": perms["options"]} + for group, perms in SCOPE_DESCRIPTIONS.items() + ], + "APIAuthToken_types": APIAuthToken.AdministratorServiceTypes, + } + + +class GenerateAPIKeyModal(Modal, PermissionModalContext): + modal_name = 'generate_api_key' + +class PassTeamIdContext: + def get_context(self, request: WebRequest) -> dict: + return { + "team_id": request.GET.get("team"), + } + +class TeamCreateUserModal(Modal, PermissionModalContext, PassTeamIdContext): + modal_name = 'team_create_user' + + def get(self, request: WebRequest): + context = self.get_context(request) + context["team_id"] = request.GET.get("team") + return self.Response(request, context) + +class CreateTeamModal(Modal): + modal_name = 'create_team' + +class EditTeamMemberPermissions(Modal, PermissionModalContext): + modal_name = 'edit_team_member_permissions' + template_name = 'modals/edit_team_member_permissions.html' + + def get(self, request: WebRequest): + context = self.get_context(request) + + team = request.user.logged_in_as_team + + if not team: + messages.error(request, "You are not logged in as a team") + return render(request, "core/base/toast.html", context) + + for_user = team.members.filter(id=request.GET.get("user")).first() + for_user_perms = team.permissions.filter(user=for_user).first() + + if not for_user: + messages.error(request, "User not found") + return render(request, "core/base/toast.html", context) + + context["editing_user"] = for_user + context["user_current_scopes"] = for_user_perms.scopes if for_user_perms else [] + + return self.Response(request, context) + +class ChangeProfilePictureModal(Modal): + modal_name = 'change_profile_picture' + + def get(self, request: WebRequest): + context = self.get_context(request) + + try: + context["users_profile_picture"] = request.user.user_profile.profile_picture_url + except UserSettings.DoesNotExist: + pass + + return self.Response(request, context) + +class LeaveTeamModal(Modal): + modal_name = 'leave_team' + + def get(self, request: WebRequest): + context = self.get_context(request) + + if request.user.teams_joined.filter(id=request.GET.get("team")).exists(): + context["team"] = Organization.objects.filter(id=request.GET.get("team")).first() + + return self.Response(request, context) + + +class InvoiceUserToTeamModal(Modal, PassTeamIdContext): + modal_name = 'invite_user_to_team' \ No newline at end of file diff --git a/src/core/service/modals/registry.py b/src/core/service/modals/registry.py new file mode 100644 index 0000000..9d18aed --- /dev/null +++ b/src/core/service/modals/registry.py @@ -0,0 +1,77 @@ +import logging +from abc import abstractmethod, ABC + +from django.http import HttpResponse +from django.shortcuts import render + +from core.types.requests import WebRequest + +modal_registry = {} + + +class Modal(ABC): + modal_name = None # Override in subclasses + template_name = None # Override in subclasses + context = {} + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + if not cls.modal_name: + raise ValueError(f"Modal class {cls.__name__} must define a `modal_name`.") + if cls.modal_name in modal_registry: + logging.info( + f"Modal {cls.modal_name} is being overridden by {cls.__name__}.", + UserWarning + ) + modal_registry[cls.modal_name] = cls + + def get_context(self, request) -> dict: + """Aggregate context data from classes that explicitly define a `get_context` method.""" + context = {} + + for base in self.__class__.mro(): + # Ensure the base has a distinct `get_context` method to avoid infinite recursion + if base is Modal: + continue + if 'get_context' in base.__dict__: + # logging.debug(f"Gathering context from: {base.__name__}") + base_context = base.get_context(self, request) + context.update(base_context) + return context + + def get(self, request: WebRequest): + """Populate the context with all inherited context mixins, then return the response.""" + context = self.get_context(request) + return render(request, self.template_name or f"modals/{self.modal_name}.html", context) + + def get_template_name(self): + return self.template_name or f"modals/{self.modal_name}.html" + + class _Response: + def __init__(self, modal_instance, request, context=None, template=None): + self.modal_instance = modal_instance + self.request = request + self.context = context or modal_instance.get_context(request) + self.template = template or modal_instance.get_template_name() + + def render(self) -> HttpResponse: + """Return the rendered response.""" + return render(self.request, self.template, self.context) + + def Response(self, request, context=None, template=None) -> HttpResponse: + """Shortcut to create and render a response.""" + return self._Response(self, request, context, template).render() + + +def get_modal(name): + return modal_registry.get(name) + +# EXAMPLE MODAL + +# class LogoutModal(Modal): +# modal_name = 'logout' +# +# def get(self, request: WebRequest, *args, **kwargs): +# context = {'message': 'Are you sure you want to log out?'} +# return self.Response(request, context) diff --git a/src/core/service/modals/template_exists.py b/src/core/service/modals/template_exists.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/permissions/__init__.py b/src/core/service/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/permissions/scopes.py b/src/core/service/permissions/scopes.py new file mode 100644 index 0000000..ce030fc --- /dev/null +++ b/src/core/service/permissions/scopes.py @@ -0,0 +1,34 @@ +from core.api.public.permissions import SCOPE_DESCRIPTIONS, SCOPES +from core.types.requests import WebRequest +from core.utils.dataclasses import BaseServiceResponse + + +class PermissionScopesServiceResponse(BaseServiceResponse[None]): + response: None = None + + +def get_permissions_from_request(request: WebRequest) -> list: + scopes = [ + f"{group}:{perm}" + for group, items in SCOPE_DESCRIPTIONS.items() + if (perm := request.POST.get(f"permission_{group}")) in items["options"] + ] + + scopes.extend(f"{group}:read" for group, items in SCOPE_DESCRIPTIONS.items() if request.POST.get(f"permission_{group}") == "write") + + return scopes + + +def validate_scopes(permissions: list[str]) -> PermissionScopesServiceResponse: + """ + Validate permissions are valid + """ + if not permissions: + return PermissionScopesServiceResponse(True) + + invalid_permissions: list[str] = [permission for permission in permissions if permission not in SCOPES] + + if invalid_permissions: + return PermissionScopesServiceResponse(False, error_message=f"Invalid permissions: {', '.join(invalid_permissions)}") + + return PermissionScopesServiceResponse(True) diff --git a/src/core/service/settings/__init__.py b/src/core/service/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/settings/update.py b/src/core/service/settings/update.py new file mode 100644 index 0000000..3266387 --- /dev/null +++ b/src/core/service/settings/update.py @@ -0,0 +1,36 @@ +from core.models import UserSettings +from PIL import Image + +from core.utils.dataclasses import BaseServiceResponse + + +class UpdateProfilePictureServiceResponse(BaseServiceResponse[str]): ... + + +def update_profile_picture(profile_picture, user_profile: UserSettings) -> UpdateProfilePictureServiceResponse: + if not profile_picture: + return UpdateProfilePictureServiceResponse(error_message="Invalid or unsupported image file") + + try: + # Max file size is 10MB (Change the first number to determine the size in MB) + max_file_size = 10 * 1024 * 1024 + + if profile_picture.size is None: + return UpdateProfilePictureServiceResponse(error_message="File size not found") + + if profile_picture.size > max_file_size: + return UpdateProfilePictureServiceResponse(error_message="File size should be up to 10MB") + + img = Image.open(profile_picture) + img.verify() + + if img.format is None or img.format.lower() not in ["jpeg", "png", "jpg"]: + return UpdateProfilePictureServiceResponse( + error_message="Unsupported image format. We support only JPEG, JPG, PNG, if you have a good extension, your file just got renamed." + ) + + user_profile.profile_picture = profile_picture + user_profile.save() + return UpdateProfilePictureServiceResponse(True, "Successfully updated profile picture") + except (FileNotFoundError, Image.UnidentifiedImageError): + return UpdateProfilePictureServiceResponse(error_message="Invalid or unsupported image file") diff --git a/src/core/service/settings/view.py b/src/core/service/settings/view.py new file mode 100644 index 0000000..5bd0cf2 --- /dev/null +++ b/src/core/service/settings/view.py @@ -0,0 +1,55 @@ +from django.db.models import QuerySet + +from core.api.public import APIAuthToken +from core.models import UserSettings +# from core.service.defaults.get import get_account_defaults +from core.types.requests import WebRequest + + +def validate_page(page: str | None) -> bool: + return not page or page in ["profile", "account", "api_keys", "account_defaults", "account_security", "email_templates"] + + +def get_user_profile(request: WebRequest) -> UserSettings: + try: + usersettings = request.user.user_profile + except UserSettings.DoesNotExist: + # Create a new UserSettings object + usersettings = UserSettings.objects.create(user=request.user) + return usersettings + + +def get_api_keys(request: WebRequest) -> QuerySet[APIAuthToken]: + return APIAuthToken.filter_by_owner(request.actor).filter(active=True).only("created", "name", "last_used", "description", "expires") + + +def account_page_context(request: WebRequest, context: dict) -> None: + user_profile = get_user_profile(request) + context.update({"currency_signs": user_profile.CURRENCIES, "currency": user_profile.currency}) + + +def api_keys_page_context(request: WebRequest, context: dict) -> None: + api_keys = get_api_keys(request) + context.update({"api_keys": api_keys}) + + +def account_defaults_context(request: WebRequest, context: dict) -> None: + ... + # context.update({"account_defaults": get_account_defaults(request.actor)}) + + +def email_templates_context(request: WebRequest, context: dict) -> None: + # acc_defaults = get_account_defaults(request.actor) + # context.update( + # { + # "account_defaults": acc_defaults, + # "email_templates": { + # "recurring_invoices": { + # "invoice_created": acc_defaults.email_template_recurring_invoices_invoice_created, + # "invoice_overdue": acc_defaults.email_template_recurring_invoices_invoice_overdue, + # "invoice_cancelled": acc_defaults.email_template_recurring_invoices_invoice_cancelled, + # } + # }, + # } + # ) + print(context.get("email_templates")) diff --git a/src/core/service/teams/__init__.py b/src/core/service/teams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/teams/create_user.py b/src/core/service/teams/create_user.py new file mode 100644 index 0000000..c50a579 --- /dev/null +++ b/src/core/service/teams/create_user.py @@ -0,0 +1,65 @@ +from textwrap import dedent + +from django.urls import reverse +from django.utils.crypto import get_random_string + +from core.models import User, Organization, TeamMemberPermission +from core.utils.dataclasses import BaseServiceResponse +from settings.helpers import send_email + + +class CreateUserServiceResponse(BaseServiceResponse[User]): ... + + +def create_user_service( + request, email: str, team: Organization, first_name: str, last_name: str, permissions: list[str] +) -> CreateUserServiceResponse: + + if not first_name: + return CreateUserServiceResponse(error_message="Please enter a valid first name") + + if not email: + return CreateUserServiceResponse(error_message="Please enter a valid user email") + + if User.objects.filter(email=email).exists(): + return CreateUserServiceResponse(error_message="This user already exists, invite them instead!") + + temporary_password = get_random_string(length=8) + + user: User = User.objects.create_user(email=email, first_name=first_name, last_name=last_name, username=email) + user.set_password(temporary_password) + user.awaiting_email_verification = False + user.require_change_password = True + user.save() + + send_email( + destination=email, + subject="You have been invited to join an organization", + content=dedent( + f""" + Hi {user.first_name or "User"}, + + You have been invited by {request.user.email} to join the organization '{team.name}'. + + Your account email is: {email} + Your temporary password is: {temporary_password} + + We suggest that you change your password as soon as you login, however no other user including the organization have + access to this password. + + Upon login, you will be added to the \"{team.name}\" organization. However, if required, you may leave at any point. + + Login to your new account using this link: + {request.build_absolute_uri(reverse("core:auth:login"))} + + Didn't give permission to be added to this organization? You can safely ignore the email, no actions can be done on + behalf of you without your permission. + """ + ), + ) + + team.members.add(user) + + TeamMemberPermission.objects.create(user=user, team=team, scopes=permissions) + + return CreateUserServiceResponse(True, response=user) diff --git a/src/core/service/teams/fetch.py b/src/core/service/teams/fetch.py new file mode 100644 index 0000000..ab782e9 --- /dev/null +++ b/src/core/service/teams/fetch.py @@ -0,0 +1,8 @@ +from django.db.models import QuerySet + +from core.models import Organization +from core.types.requests import WebRequest + + +def get_all_users_teams(request: WebRequest) -> QuerySet[Organization]: + return request.user.teams_joined.all() | request.user.teams_leader_of.all() diff --git a/src/core/service/teams/permissions.py b/src/core/service/teams/permissions.py new file mode 100644 index 0000000..e9047f0 --- /dev/null +++ b/src/core/service/teams/permissions.py @@ -0,0 +1,47 @@ +from core.models import User, Organization, TeamMemberPermission +from core.service.permissions.scopes import validate_scopes +from core.utils.dataclasses import BaseServiceResponse + + +class EditMemberPermissionsServiceResponse(BaseServiceResponse[None]): + response: None = None + + +def edit_member_permissions(receiver: User, team: Organization | None, permissions: list) -> EditMemberPermissionsServiceResponse: + if not validate_receiver(receiver, team): + return EditMemberPermissionsServiceResponse(error_message="Invalid key name") + + if (scopes_response := validate_scopes(permissions)).failed: + return EditMemberPermissionsServiceResponse(error_message=scopes_response.error) + + if not team: + return EditMemberPermissionsServiceResponse(error_message="Invalid team, something went wrong") + + user_team_perms: TeamMemberPermission | None = team.permissions.filter(user=receiver).first() + + if not user_team_perms: + team.permissions.add(TeamMemberPermission.objects.create(user=receiver, team=team, scopes=permissions)) + else: + user_team_perms.scopes = permissions + user_team_perms.save() + + return EditMemberPermissionsServiceResponse(True) + + +def validate_receiver(receiver: User | None, team: Organization | None) -> bool: + """ + Make sure receiver is in team and not already owner + """ + + if not receiver: + return False + + if not team: + return False + + if not team.members.filter(id=receiver.id).first(): + return False + + if not team.leader == receiver: + return True + return False diff --git a/src/core/service/webhooks/__init__.py b/src/core/service/webhooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/service/webhooks/auth.py b/src/core/service/webhooks/auth.py new file mode 100644 index 0000000..7821746 --- /dev/null +++ b/src/core/service/webhooks/auth.py @@ -0,0 +1,33 @@ +from core.api.public import APIAuthToken +from core.types.requests import WebRequest +from core.utils.dataclasses import BaseServiceResponse + + +class APIAuthenticationServiceResponse(BaseServiceResponse[None]): + response: None = None + _status_code: int + + +def authenticate_api_key(request: WebRequest) -> APIAuthenticationServiceResponse: + auth_header = request.headers.get("Authorization") + + if not (auth_header and auth_header.startswith("Bearer ")): + return APIAuthenticationServiceResponse(error_message="Unauthorized", status_code=401) + + token_key = auth_header.split(" ")[1] + + try: + token = APIAuthToken.objects.get( + hashed_key=APIAuthToken.hash_raw_key(token_key), + active=True, + administrator_service_type=APIAuthToken.AdministratorServiceTypes.AWS_WEBHOOK_CALLBACK, + ) + + if token.has_expired: + return APIAuthenticationServiceResponse(error_message="Token expired", status_code=400) + except APIAuthToken.DoesNotExist: + return APIAuthenticationServiceResponse(error_message="Token not found", status_code=400) + + token.update_last_used() + + return APIAuthenticationServiceResponse(True, None, status_code=200) diff --git a/src/core/settings.py b/src/core/settings.py new file mode 100644 index 0000000..3a246ba --- /dev/null +++ b/src/core/settings.py @@ -0,0 +1,28 @@ +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.staticfiles', + 'core', + 'billing' +] + +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, + "public_media": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + "OPTIONS": {"location": "media/public", "base_url": "/media/public/"}, + }, + "private_media": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + "OPTIONS": {"location": "media/private", "base_url": "/media/private/"}, + }, +} + +AUTH_USER_MODEL = "core.User" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" \ No newline at end of file diff --git a/src/core/signals/__init__.py b/src/core/signals/__init__.py new file mode 100644 index 0000000..6e946f5 --- /dev/null +++ b/src/core/signals/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + +from . import migrations +from . import signals diff --git a/src/core/signals/migrations.py b/src/core/signals/migrations.py new file mode 100644 index 0000000..de3987e --- /dev/null +++ b/src/core/signals/migrations.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import logging + +from django.db.models.signals import post_migrate +from django.dispatch import receiver + +from core.data.default_feature_flags import default_feature_flags +from core.models import FeatureFlags + + +@receiver(post_migrate) +def update_feature_flags(**kwargs): + for feature_flag in default_feature_flags: + existing_item = FeatureFlags.objects.filter(name=feature_flag.name).first() + + if existing_item: + name, value, description = ( + existing_item.name, + existing_item.value, + existing_item.description, + ) + + existing_item.name = name + existing_item.description = description + + if existing_item.name != name or existing_item.description != description: + existing_item.save() + logging.info(f"Updated feature flag: {feature_flag.name}") + else: + FeatureFlags.objects.create(name=feature_flag.name, value=feature_flag.default, description=feature_flag.description) + logging.info(f"Added feature flag: {feature_flag.name}") + +# +# @receiver(post_migrate) +# def update_quota_limits(**kwargs): +# for group in default_quota_limits: +# for item in group.items: +# existing = QuotaLimit.objects.filter(slug=f"{group.name}-{item.slug}").first() +# if existing: +# name, value, adjustable, description, limit_type = ( +# existing.name, +# existing.value, +# existing.adjustable, +# existing.description, +# existing.limit_type, +# ) +# existing.name = item.name +# existing.adjustable = item.adjustable +# existing.description = item.description +# existing.limit_type = item.period +# if ( +# item.name != name +# or item.default_value != value +# or item.adjustable != adjustable +# or item.description != description +# or item.period != limit_type +# ): +# logging.info(f"Updated QuotaLimit {item.name}") +# existing.save() +# else: +# QuotaLimit.objects.create( +# name=item.name, +# slug=f"{group.name}-{item.slug}", +# value=item.default_value, +# adjustable=item.adjustable, +# description=item.description, +# limit_type=item.period, +# ) +# logging.info(f"Added QuotaLimit {item.name}") diff --git a/src/core/signals/signals.py b/src/core/signals/signals.py new file mode 100644 index 0000000..f84fcc9 --- /dev/null +++ b/src/core/signals/signals.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from django.conf import settings +from django.core.cache import cache +from django.core.cache.backends.redis import RedisCacheClient + +from core.utils.settings_helper import send_email + +cache: RedisCacheClient = cache +from django.core.files.storage import default_storage +from django.db.models.signals import pre_save, post_delete, post_save, pre_delete +from django.dispatch import receiver +from django.urls import reverse + +from core.models import UserSettings, User, FeatureFlags, VerificationCodes + +@receiver(pre_save, sender=UserSettings) +def delete_old_profile_picture(sender, instance, **kwargs): + if not instance.pk: + return + + try: + old_profile = UserSettings.objects.get(pk=instance.pk) + except UserSettings.DoesNotExist: + return + + if old_profile.profile_picture and old_profile.profile_picture != instance.profile_picture: + # If the profile picture has been updated, delete the old file + old_profile.profile_picture.delete(save=False) + + +@receiver(post_delete, sender=UserSettings) +def set_profile_picture_to_none(sender, instance, **kwargs): + # Check if the file exists in the storage + if instance.profile_picture and default_storage.exists(instance.profile_picture.name): + instance.profile_picture.delete(save=False) + +@receiver(post_save, sender=User) +def user_account_create_make_usersettings(sender, instance, created, **kwargs): + if created: + try: + users_settings = instance.user_profile + except UserSettings.DoesNotExist: + users_settings = None + + if not users_settings: + UserSettings.objects.create(user=instance) + + +@receiver(post_save, sender=FeatureFlags) +def refresh_feature_cache(sender, instance: FeatureFlags, **kwargs): + feature = instance.name + key = f"myfinances:feature_flag:{feature}" + + cached_value = cache.get(key) + + if cached_value: + return cache.delete(key) + + +@receiver(post_save, sender=User) +def send_welcome_email(sender, instance: User, created, **kwargs): + if created: + email_message = f""" + Welcome to MyFinances{f", {instance.first_name}" if instance.first_name else ""}! + + We're happy to have you join us. We are still in development and working on the core features. + + In app we have a live chat, so please drop us a message or email support@myfinances.cloud if you have any queries. + + Thank you for using MyFinances! + """ + magic_link = VerificationCodes.objects.create(user=instance, service="create_account") + token_plain = magic_link.token + magic_link.hash_token() + magic_link_url = settings.SITE_URL + reverse( + "core:auth:login create_account verify", kwargs={"uuid": magic_link.uuid, "token": token_plain} + ) + email_message += f""" + To start with, you must first **verify this email** so that we can link your account to this email. + Click the link below to activate your account, no details are required, once pressed you're all set! + + Verify Link: {magic_link_url} + """ + + email = send_email(destination=instance.email, subject="Welcome to MyFinances", content=email_message) + + # User.send_welcome_email(instance) diff --git a/src/core/templates/core/base/+left_drawer.html b/src/core/templates/core/base/+left_drawer.html new file mode 100644 index 0000000..69b0e8b --- /dev/null +++ b/src/core/templates/core/base/+left_drawer.html @@ -0,0 +1,84 @@ +{% load feature_enabled %} +{% feature_enabled "areUserEmailsAllowed" as are_user_emails_allowed %} +{#{% personal_feature_enabled request.user "invoices" as feature_enabled_invoices %}#} +{#{% personal_feature_enabled request.user "receipts" as feature_enabled_receipts %}#} +{#{% personal_feature_enabled request.user "email_sending" as feature_enabled_emails %}#} +
+ + +
diff --git a/src/core/templates/core/base/_head.html b/src/core/templates/core/base/_head.html new file mode 100644 index 0000000..2e634e3 --- /dev/null +++ b/src/core/templates/core/base/_head.html @@ -0,0 +1,70 @@ +{% load static %} +{% load render_bundle from webpack_loader %} +{% load tz_detect %} + + + + My Finances + + + + + + {% if import_method == "public_cdn" %} + + + + + {# #} + + + + {# #} + + + {# #} + + {# #} + {# + #} + + {# #} + {# Not in use at the moment MAY USE LATER ^^ #} + + + + {# #} + + {% else %} + {# #} + {# #} + {# #} + {# {% render_bundle 'all' 'js' %}#} + {% render_bundle 'init' 'js' %} + {% render_bundle 'tableify' 'js' %} + {% render_bundle 'htmx' 'js' %} + {% render_bundle 'font_awesome' 'js' %} + + + {% endif %} + {% render_bundle 'receipt_downloads' 'js' %} + {{ analytics|safe }} + {% tz_detect %} + {# #} + diff --git a/src/core/templates/core/base/auth.html b/src/core/templates/core/base/auth.html new file mode 100644 index 0000000..e64fc7c --- /dev/null +++ b/src/core/templates/core/base/auth.html @@ -0,0 +1,41 @@ +{% load static %} + +{% block head %}{% include 'core/base/_head.html' %}{% endblock %} + +
+
+
+
+
+
+

Dashboard

+
+ +
+ {% block details %} +

+ What do you get to manage? +

+

✓ Client Lists

+

✓ Invoices

+

✓ Receipt Storage

+

✓ Financial Reports

+ {% endblock %} +
+
+
+
+ {# {% component "messages_list" %}#} + {% include "core/base/toasts.html" %} +

+ {% block title %} + {% endblock title %} +

+ {% block content %} + {% endblock content %} +
+
+
+
+ + diff --git a/src/core/templates/core/base/base.html b/src/core/templates/core/base/base.html new file mode 100644 index 0000000..603a3ed --- /dev/null +++ b/src/core/templates/core/base/base.html @@ -0,0 +1,55 @@ + + +{% block head %}{% include 'core/base/_head.html' %}{% endblock head %} + +
+ +
+ {% block topbar %}{% include 'core/base/topbar/_topbar.html' %}{% endblock %} +
+
+ {% include 'core/base/breadcrumbs.html' with breadcrumb_first_load=True %} +
{% include 'core/base/toasts.html' %}
+ +
+ +
+ {% block content %} + {% endblock content %} +
+ {% block drawer %}{% include "core/base/+left_drawer.html" %}{% endblock drawer %} +
+ +
+ + +
+
+ + diff --git a/src/core/templates/core/base/breadcrumbs.html b/src/core/templates/core/base/breadcrumbs.html new file mode 100644 index 0000000..e8c55a0 --- /dev/null +++ b/src/core/templates/core/base/breadcrumbs.html @@ -0,0 +1,14 @@ +{% if breadcrumb or swap or breadcrumb_first_load %} + +{% endif %} diff --git a/src/core/templates/core/base/breadcrumbs_ul.html b/src/core/templates/core/base/breadcrumbs_ul.html new file mode 100644 index 0000000..12ea2ea --- /dev/null +++ b/src/core/templates/core/base/breadcrumbs_ul.html @@ -0,0 +1,11 @@ + diff --git a/src/core/templates/core/base/htmx.html b/src/core/templates/core/base/htmx.html new file mode 100644 index 0000000..0e2e434 --- /dev/null +++ b/src/core/templates/core/base/htmx.html @@ -0,0 +1,11 @@ +
+ {% block content %} + {% endblock content %} +
+{# Profile Picture dropdown item #} +
+ {% include "core/base/topbar/+icon_dropdown.html" %} +
+{% block left_drawer %}{% include "core/base/+left_drawer.html" with swap=True %}{% endblock %} +{% block breadcrumbs %}{% include "core/base/breadcrumbs.html" with swap=True %}{% endblock %} +{% block toasts %}{% include "core/base/toasts.html" %}{% endblock %} diff --git a/src/core/templates/core/base/toast.html b/src/core/templates/core/base/toast.html new file mode 100644 index 0000000..bb637a6 --- /dev/null +++ b/src/core/templates/core/base/toast.html @@ -0,0 +1 @@ +{% component "messages_list" %} diff --git a/src/core/templates/core/base/toasts.html b/src/core/templates/core/base/toasts.html new file mode 100644 index 0000000..f2b048c --- /dev/null +++ b/src/core/templates/core/base/toasts.html @@ -0,0 +1 @@ +{% component "messages_list" with_js=True %} diff --git a/src/core/templates/core/base/topbar/+icon_dropdown.html b/src/core/templates/core/base/topbar/+icon_dropdown.html new file mode 100644 index 0000000..4e45995 --- /dev/null +++ b/src/core/templates/core/base/topbar/+icon_dropdown.html @@ -0,0 +1,47 @@ +{% load has_module safe_url from feature_enabled %} +{% has_module "billing" as billing_enabled %} +
+
  • + + + Settings + +
  • +
  • + + + Manage Team + +
  • + {# {% if billing_enabled %}#} +
  • + + + Billing Dashboard + +
  • + {# {% endif %}#} +
  • + +
  • +
    + {% if request.user.is_superuser and request.user.is_staff %} +
  • + Go to admin dashboard +
  • + {% endif %} +
  • + Current Version: {{ version }} +
  • + {% if git_version and git_version != "prod" %} +
  • + {{ git_version | slice:7 }} +
  • + {% endif %} +
    diff --git a/src/core/templates/core/base/topbar/_notification_count.html b/src/core/templates/core/base/topbar/_notification_count.html new file mode 100644 index 0000000..eba8640 --- /dev/null +++ b/src/core/templates/core/base/topbar/_notification_count.html @@ -0,0 +1,6 @@ +

    {{ notif_count | default:request.user.notification_count }}

    diff --git a/src/core/templates/core/base/topbar/_notification_dropdown_items.html b/src/core/templates/core/base/topbar/_notification_dropdown_items.html new file mode 100644 index 0000000..9a66a63 --- /dev/null +++ b/src/core/templates/core/base/topbar/_notification_dropdown_items.html @@ -0,0 +1,89 @@ +{#{% for notification in notifications.normal %}#} +{#
  • #} +{# {{ notification.message }}#} +{#
  • #} +{#{% endfor %}#} +{#{% for notification in notifications.redirect %}#} +{#
  • #} +{# {{ notification.message }}#} +{#
  • #} +{#{% endfor %}#} +{#{% for notification in notifications.modal %}#} +{#
  • #} +{# #} +{#
  • #} +{#{% endfor %}#} +{#{% if not notifications.modal and not notifications.redirect and not notifications.normal %}#} +{#
  • #} +{# #} +{#
  • #} +{#{% endif %}#} +{% for notification in notifications %} + {% if notification.action == "normal" %} +
  • + {{ notification.message }} +
  • + {% elif notification.action == "redirect" %} +
  • +
    + {{ notification.message }} + + + + +
    +
  • + {% elif notification.action == "modal" %} +
  • + +
  • + {% endif %} +{% empty %} +
  • + +
  • +{% endfor %} +{% if notif_count > 5 %} +
  • + + You have older notifications. +
    + Clear some to see more. +
    +
  • +{% endif %} +{% if not initial_load %} + {% include "core/base/topbar/_notification_count.html" %} +{% endif %} diff --git a/src/core/templates/core/base/topbar/_organizations_list.html b/src/core/templates/core/base/topbar/_organizations_list.html new file mode 100644 index 0000000..0816189 --- /dev/null +++ b/src/core/templates/core/base/topbar/_organizations_list.html @@ -0,0 +1,15 @@ +{% for team in request.user.teams_joined.all %} +
  • + {{ team.name | title }} +
  • +{% endfor %} +{% for team in request.user.teams_leader_of.all %} +
  • + {{ team.name | title }} +
  • +{% endfor %} +
  • + Personal +
  • diff --git a/src/core/templates/core/base/topbar/_topbar.html b/src/core/templates/core/base/topbar/_topbar.html new file mode 100644 index 0000000..8466aea --- /dev/null +++ b/src/core/templates/core/base/topbar/_topbar.html @@ -0,0 +1,128 @@ +{% load static %} + diff --git a/src/core/templates/core/base/topbar/team_selector/selector.html b/src/core/templates/core/base/topbar/team_selector/selector.html new file mode 100644 index 0000000..046d2a5 --- /dev/null +++ b/src/core/templates/core/base/topbar/team_selector/selector.html @@ -0,0 +1,52 @@ + diff --git a/src/core/templates/modals/accept_invite.html b/src/core/templates/modals/accept_invite.html new file mode 100644 index 0000000..4ebe2ce --- /dev/null +++ b/src/core/templates/modals/accept_invite.html @@ -0,0 +1,22 @@ +{% component_block "modal" id="modal_accept_invite" start_open="true" title="Accept Invitation" %} +{% fill "content" %} + +{% endfill %} +{% endcomponent_block %} diff --git a/src/core/templates/modals/change_profile_picture.html b/src/core/templates/modals/change_profile_picture.html new file mode 100644 index 0000000..2843d94 --- /dev/null +++ b/src/core/templates/modals/change_profile_picture.html @@ -0,0 +1,45 @@ +{% component_block "modal" id="modal_change_profile_picture" start_open="true" title="Change profile picture" %} +{% fill "content" %} +