commit 13b66f57710e70828732850136ac376633f8ac8e Author: Ēriks K Date: Thu Dec 26 14:42:24 2024 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff90268 --- /dev/null +++ b/.gitignore @@ -0,0 +1,177 @@ +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +.idea/sonarlint/ +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media +docs/_book +test/ +logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +pids +*.pid +*.seed +*.pid.lock +lib-cov +coverage +*.lcov +.nyc_output +.grunt +bower_components +.lock-wscript +build/Release +node_modules/ +jspm_packages/ +web_modules/ +*.tsbuildinfo +.npm +.eslintcache +.stylelintcache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ +.node_repl_history +*.tgz +.yarn-integrity +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +.cache +.parcel-cache +.next +out +.nuxt +dist +.cache/ +.vuepress/dist +.temp +.docusaurus +.serverless/ +.fusebox/ +.dynamodb/ +.tern-port +.vscode-test +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* +.yarn/* +!.yarn/releases +!.yarn/patches +!.yarn/plugins +!.yarn/sdks +!.yarn/versions +!.yarn/cache +dist/ +npm-debug.log +yarn-error.log +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +instance/ +.webassets-cache +.scrapy +docs/_build/ +.pybuilder/ +target/ +.ipynb_checkpoints +profile_default/ +ipython_config.py +.pdm.toml +.pdm-python +.pdm-build/ +__pypackages__/ +celerybeat-schedule +celerybeat.pid +*.sage.py +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.spyderproject +.spyproject +.ropeproject +/site +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/fastbackend_template.iml b/.idea/fastbackend_template.iml new file mode 100644 index 0000000..d4ef807 --- /dev/null +++ b/.idea/fastbackend_template.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1c878db --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..eedbd60 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/cookiecutter.json b/cookiecutter.json new file mode 100644 index 0000000..deb8f97 --- /dev/null +++ b/cookiecutter.json @@ -0,0 +1,6 @@ +{ + "project_name": "FastAPI Backend", + "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}", + "author": "keriks" +} + diff --git a/cookiecutter.zip b/cookiecutter.zip new file mode 100644 index 0000000..db78f5e Binary files /dev/null and b/cookiecutter.zip differ diff --git a/{{ cookiecutter.project_slug }}/.dockerignore b/{{ cookiecutter.project_slug }}/.dockerignore new file mode 100644 index 0000000..7369480 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/.dockerignore @@ -0,0 +1,11 @@ +.editorconfig +.gitattributes +.github +.gitignore +.gitlab-ci.yml +.idea +.pre-commit-config.yaml +.readthedocs.yml +.travis.yml +venv +.git diff --git a/{{ cookiecutter.project_slug }}/.editorconfig b/{{ cookiecutter.project_slug }}/.editorconfig new file mode 100644 index 0000000..8fdd1f4 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org + +root = true + +[*] +max_line_length = 120 +indent_style = space +indent_size = 4 +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{html,css,scss,json,yml,yaml,xml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/{{ cookiecutter.project_slug }}/.flake8 b/{{ cookiecutter.project_slug }}/.flake8 new file mode 100644 index 0000000..a3cf33b --- /dev/null +++ b/{{ cookiecutter.project_slug }}/.flake8 @@ -0,0 +1,4 @@ +[flake8] +extend-ignore = E203, E701, E704, W605, F405 +exclude = .git,__pycache__,venv,migrations +max-line-length = 120 diff --git a/{{ cookiecutter.project_slug }}/.gitignore b/{{ cookiecutter.project_slug }}/.gitignore new file mode 100644 index 0000000..577c395 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/.gitignore @@ -0,0 +1,117 @@ +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +instance/ +.webassets-cache +.scrapy +docs/_build/ +.pybuilder/ +target/ +.ipynb_checkpoints +profile_default/ +ipython_config.py +.pdm.toml +__pypackages__/ +celerybeat-schedule +celerybeat.pid +*.sage.py +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.spyderproject +.spyproject +.ropeproject +/site +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ +/db.sqlite +/db.sqlite-shm +/db.sqlite-wal +/envs.yml diff --git a/{{ cookiecutter.project_slug }}/Dockerfile b/{{ cookiecutter.project_slug }}/Dockerfile new file mode 100644 index 0000000..a3280df --- /dev/null +++ b/{{ cookiecutter.project_slug }}/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-alpine as python +WORKDIR /app +ENV PYTHONUNBUFFERED 1 +ENV TZ="Europe/Riga" + +COPY requirements /app/requirements +RUN apk add libpq \ + poppler-utils zlib-dev \ +# curl \ + && pip install --no-cache-dir --upgrade -r requirements/production.txt \ + && rm -rf /var/cache/apk/* + +COPY entrypoint.sh /entrypoint +COPY . /app +RUN chmod +x /entrypoint \ + && sed -i 's/\r$//g' /entrypoint + +# Default listening address - 0.0.0.0:5000 +EXPOSE 5000 + +ENTRYPOINT ["/entrypoint"] +CMD ["uvicorn", "--workers", "2", "--proxy-headers", "--host", "0.0.0.0", "--port", "5000", "--forwarded-allow-ips=*", "service.main:app"] diff --git a/{{ cookiecutter.project_slug }}/ENVIRON.md b/{{ cookiecutter.project_slug }}/ENVIRON.md new file mode 100644 index 0000000..e8a497d --- /dev/null +++ b/{{ cookiecutter.project_slug }}/ENVIRON.md @@ -0,0 +1,63 @@ +# `SERVICE_ROOT_PATH` + +*Optional*, default value: `` + +## Examples + +`/api/v2`, `` + +# `SERVICE_PROJECT_NAME` + +*Optional*, default value: `logger-be` + +# `SERVICE_DEBUG` + +*Optional*, default value: `False` + +# `SERVICE_SECRET_KEY` + +*Optional*, default value: `CHANGE_ME--8^&gnoqen9+&9usjpjnsw*lhfqnl45p!^hdvf*s*i--INSECURE` + +# `SERVICE_TOKEN_EXPIRATION_DAYS` + +*Optional*, default value: `1` + +# `SERVICE_CORS_ORIGINS` + +*Optional*, default value: `[]` + +# `SERVICE_ENVIRONMENT` + +*Optional*, default value: `local` + +## Examples + +`local`, `testing`, `staging`, `production` + +# `SERVICE_LOG_LEVEL` + +*Optional*, default value: `INFO` + +## Examples + +`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` + +# `SERVICE_REDIS_URL` + +*Optional*, default value: `redis://redis:6379` + +# `SERVICE_DATABASE_URL` + +*Optional*, default value: `psql://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}` + +# `SERVICE_TIMEZONE` + +*Optional*, default value: `Europe/Riga` + +## Examples + +`UTC`, `Europe/Riga`, `Europe/London`, `US/Pacific` + +# `SERVICE_SENTRY_URL` + +*Optional*, default value: `None` diff --git a/{{ cookiecutter.project_slug }}/Makefile b/{{ cookiecutter.project_slug }}/Makefile new file mode 100644 index 0000000..b6f9611 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/Makefile @@ -0,0 +1,71 @@ +.PHONY: clean clean/build clean/pyc help lint lint/flake8 lint/black lint/isort lint/type test run-tasks run-app envs envs/generate-md envs/generate-yaml +.DEFAULT_GOAL := help + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean/build clean/pyc ## remove all build, test, coverage and Python artifacts + +clean/build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean/pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + find . -name '.pytest_cache' -exec rm -fr {} + + + +lint/flake8: ## check style with flake8 + flake8 service +lint/black: ## check style with black + black service +lint/isort: ## check imports with isort + isort service +lint/type: ## check style with black + mypy service +lint: lint/isort lint/black lint/flake8 lint/type ## check style + + +test: ## run tests quickly with the default Python + pytest -v + + +docker-build: + docker build --progress plain --tag={{ cookiecutter.project_slug }}:latest . +docker-release: + docker build --tag={{ cookiecutter.project_slug }}:release . +docker-push: docker-build + docker push {{ cookiecutter.project_slug }}:latest + + +run-app: + exec uvicorn --reload --host 0.0.0.0 --port 8181 --proxy-headers --forwarded-allow-ips=* service.main:app +run-tasks: + exec dramatiq service.tasks --processes=1 --threads=2 --watch config --watch service + + +envs: envs/generate-md envs/generate-yaml +envs/generate-yaml: + #settings-doc generate --class service.config.ProjectSettings --output-format dotenv > example.env + settings-doc generate --class service.config.ProjectSettings --output-format dotenv --update example.env +envs/generate-md: + #settings-doc generate --class service.config.ProjectSettings --output-format markdown > ENVIRON.md + settings-doc generate --class service.config.ProjectSettings --output-format markdown --update ENVIRON.md diff --git a/{{ cookiecutter.project_slug }}/compose/base/prestart.sh b/{{ cookiecutter.project_slug }}/compose/base/prestart.sh new file mode 100644 index 0000000..4ac6365 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/compose/base/prestart.sh @@ -0,0 +1,12 @@ +#! /usr/bin/env sh + +# Let the DB start +python backend_pre_start.py + +# Run migrations +aerich upgrade + +# Create initial data in DB +if [ -n "$DEBUG" ]; then + python initial_data.py +fi diff --git a/{{ cookiecutter.project_slug }}/docker-compose.yaml b/{{ cookiecutter.project_slug }}/docker-compose.yaml new file mode 100644 index 0000000..c39696e --- /dev/null +++ b/{{ cookiecutter.project_slug }}/docker-compose.yaml @@ -0,0 +1,28 @@ +services: + app: + build: + context: . + dockerfile: compose/dev/Dockerfile + volumes: + - logs:/app/logs + - files:/app/media + - .:/app + depends_on: + - postgres + - redis + + postgres: + image: postgres:16-alpine + restart: always + env_file: + - .env + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:alpine + +volumes: + logs: { } + files: { } + postgres_data: { } diff --git a/{{ cookiecutter.project_slug }}/envs.example.yml b/{{ cookiecutter.project_slug }}/envs.example.yml new file mode 100644 index 0000000..8f3e8b0 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/envs.example.yml @@ -0,0 +1,19 @@ +service: + root_path: "/api/v1" + project_name: {{ cookiecutter.project_name }} + debug: true + + secret_key: ChangeME + token_expiration_days: 28 + cors_origins: + - "http://localhost:5000" + - "https://example.com" + + environment: "local" + log_level: "DEBUG" + + redis_url: "redis://redis:6379" + database_url: "psql://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}" + + timezone: "UTC" + sentry_url: null diff --git a/{{ cookiecutter.project_slug }}/export_fixtures.py b/{{ cookiecutter.project_slug }}/export_fixtures.py new file mode 100644 index 0000000..9c67498 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/export_fixtures.py @@ -0,0 +1,30 @@ +import time +from typing import Sequence, Type + +import orjson +from tortoise import Model, Tortoise, run_async + +from service.config import TORTOISE_ORM +from service.database.models import User + + +async def do_export(): + export_ts = int(time.time()) + models: Sequence[Type[Model]] = (User,) + await Tortoise.init(TORTOISE_ORM) + for filename, model in ((model._meta.db_table, model) for model in models): + all_objects = await model.all().order_by("id").values() + if all_objects: + with open(f"fixtures/{export_ts}__{filename}.json", "wb") as f: + f.write(orjson.dumps(all_objects)) + + +def main(): + start_ts = time.time() + run_async(do_export()) + end_ts = time.time() + print(f"Export completed in {end_ts - start_ts:.3f}s") + + +if __name__ == "__main__": + main() diff --git a/{{ cookiecutter.project_slug }}/fixtures/user.json b/{{ cookiecutter.project_slug }}/fixtures/user.json new file mode 100644 index 0000000..4ce616e --- /dev/null +++ b/{{ cookiecutter.project_slug }}/fixtures/user.json @@ -0,0 +1,1602 @@ +[ + { + "created_at": "2024-04-21T14:13:15.051665+00:00", + "modified_at": "2024-04-21T14:13:15.052146+00:00", + "email": "46a92809f668441c9bfdd6fe61a548bc@example.com", + "username": "46a92809f668441c9bfdd6fe61a548bc", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.094283+00:00", + "modified_at": "2024-04-21T14:13:15.094318+00:00", + "email": "3929302f51d747bdabe2b2b268bfcd9a@example.com", + "username": "3929302f51d747bdabe2b2b268bfcd9a", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.136892+00:00", + "modified_at": "2024-04-21T14:13:15.136925+00:00", + "email": "350c244bb2f94ac9917c546051f6309c@example.com", + "username": "350c244bb2f94ac9917c546051f6309c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.179731+00:00", + "modified_at": "2024-04-21T14:13:15.179763+00:00", + "email": "3d498deeb6774c5f9c67e24e57b83fb1@example.com", + "username": "3d498deeb6774c5f9c67e24e57b83fb1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.222691+00:00", + "modified_at": "2024-04-21T14:13:15.222723+00:00", + "email": "84195d5067994fd2abb9bdb51224a004@example.com", + "username": "84195d5067994fd2abb9bdb51224a004", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.266039+00:00", + "modified_at": "2024-04-21T14:13:15.266067+00:00", + "email": "db1d6abb407544688ba718fd53fc8d8e@example.com", + "username": "db1d6abb407544688ba718fd53fc8d8e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.310100+00:00", + "modified_at": "2024-04-21T14:13:15.310123+00:00", + "email": "ed708e466ea542938a97d3797347a524@example.com", + "username": "ed708e466ea542938a97d3797347a524", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.532831+00:00", + "modified_at": "2024-04-21T14:13:15.532856+00:00", + "email": "3b6d0741f8ec45eebecd6b6d1484c6c9@example.com", + "username": "3b6d0741f8ec45eebecd6b6d1484c6c9", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.577982+00:00", + "modified_at": "2024-04-21T14:13:15.578034+00:00", + "email": "0a7e48da47cf4a7f95baccdd55546cfd@example.com", + "username": "0a7e48da47cf4a7f95baccdd55546cfd", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.621775+00:00", + "modified_at": "2024-04-21T14:13:15.621808+00:00", + "email": "b33cbf3dab564638a7a03435eab04de4@example.com", + "username": "b33cbf3dab564638a7a03435eab04de4", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.666471+00:00", + "modified_at": "2024-04-21T14:13:15.666492+00:00", + "email": "528ac73b359c43129bc80d720d51f992@example.com", + "username": "528ac73b359c43129bc80d720d51f992", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.709133+00:00", + "modified_at": "2024-04-21T14:13:15.709160+00:00", + "email": "3d736188733f469ba0b7b784f8beae0f@example.com", + "username": "3d736188733f469ba0b7b784f8beae0f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.767373+00:00", + "modified_at": "2024-04-21T14:13:15.767397+00:00", + "email": "94e76715a5b84f3791ec4b1569bf9beb@example.com", + "username": "94e76715a5b84f3791ec4b1569bf9beb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.809041+00:00", + "modified_at": "2024-04-21T14:13:15.809068+00:00", + "email": "d865fada615149ea9668c67e9dd28d56@example.com", + "username": "d865fada615149ea9668c67e9dd28d56", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:15.924343+00:00", + "modified_at": "2024-04-21T14:13:15.924368+00:00", + "email": "d64c979c48bb4913a76c568feab65718@example.com", + "username": "d64c979c48bb4913a76c568feab65718", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.055074+00:00", + "modified_at": "2024-04-21T14:13:16.055100+00:00", + "email": "159b2ce62a5141a488b44fddfda196f5@example.com", + "username": "159b2ce62a5141a488b44fddfda196f5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.099738+00:00", + "modified_at": "2024-04-21T14:13:16.099775+00:00", + "email": "37e0bbceb09a4352af1fad96378af518@example.com", + "username": "37e0bbceb09a4352af1fad96378af518", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.143833+00:00", + "modified_at": "2024-04-21T14:13:16.143864+00:00", + "email": "1cbd67abe78c470e86700043d08fad57@example.com", + "username": "1cbd67abe78c470e86700043d08fad57", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.185784+00:00", + "modified_at": "2024-04-21T14:13:16.185810+00:00", + "email": "4ee5ae90b52a4a079a6e363a5d21ab54@example.com", + "username": "4ee5ae90b52a4a079a6e363a5d21ab54", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.228727+00:00", + "modified_at": "2024-04-21T14:13:16.228751+00:00", + "email": "2613b00d9a674ce6ad3e1e032d68194f@example.com", + "username": "2613b00d9a674ce6ad3e1e032d68194f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.273191+00:00", + "modified_at": "2024-04-21T14:13:16.273221+00:00", + "email": "2195a58e8c504bbab7b64119f24cd6e6@example.com", + "username": "2195a58e8c504bbab7b64119f24cd6e6", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.316994+00:00", + "modified_at": "2024-04-21T14:13:16.317022+00:00", + "email": "9c110b7bfd2043e789ab34451292300b@example.com", + "username": "9c110b7bfd2043e789ab34451292300b", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.783546+00:00", + "modified_at": "2024-04-21T14:13:16.783578+00:00", + "email": "a324b00d0f5141a5a68b28ba152ae3b4@example.com", + "username": "a324b00d0f5141a5a68b28ba152ae3b4", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.825295+00:00", + "modified_at": "2024-04-21T14:13:16.825332+00:00", + "email": "1bc5ef3a73034892a11a535cfe108025@example.com", + "username": "1bc5ef3a73034892a11a535cfe108025", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:16.867079+00:00", + "modified_at": "2024-04-21T14:13:16.867111+00:00", + "email": "7f7bda28a42e46c79268e5e0cfe8f03e@example.com", + "username": "7f7bda28a42e46c79268e5e0cfe8f03e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.299057+00:00", + "modified_at": "2024-04-21T14:13:17.299079+00:00", + "email": "2bc21f9c06954da4a10aa2bd2f80b962@example.com", + "username": "2bc21f9c06954da4a10aa2bd2f80b962", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.349949+00:00", + "modified_at": "2024-04-21T14:13:17.349969+00:00", + "email": "9795cad043ae46e3a344c78f4bae7fb8@example.com", + "username": "9795cad043ae46e3a344c78f4bae7fb8", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.392046+00:00", + "modified_at": "2024-04-21T14:13:17.392079+00:00", + "email": "250d8ff8c2c5494abf40c9e668262257@example.com", + "username": "250d8ff8c2c5494abf40c9e668262257", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.629118+00:00", + "modified_at": "2024-04-21T14:13:17.629144+00:00", + "email": "9f5461010ee94235b3e25bc8d671f4f2@example.com", + "username": "9f5461010ee94235b3e25bc8d671f4f2", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.673377+00:00", + "modified_at": "2024-04-21T14:13:17.673410+00:00", + "email": "5afd3fffb4d64e88bcc20b789ff965a0@example.com", + "username": "5afd3fffb4d64e88bcc20b789ff965a0", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.715841+00:00", + "modified_at": "2024-04-21T14:13:17.715877+00:00", + "email": "208113c993f248feb7eb52f143a9abe3@example.com", + "username": "208113c993f248feb7eb52f143a9abe3", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.757462+00:00", + "modified_at": "2024-04-21T14:13:17.757496+00:00", + "email": "c2d89d77bfba46b4856b8d9822e9152b@example.com", + "username": "c2d89d77bfba46b4856b8d9822e9152b", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.802559+00:00", + "modified_at": "2024-04-21T14:13:17.802580+00:00", + "email": "8de317d99e21489886c516cef209209f@example.com", + "username": "8de317d99e21489886c516cef209209f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.844541+00:00", + "modified_at": "2024-04-21T14:13:17.844568+00:00", + "email": "eda87756947b45119c603ff1041ebdba@example.com", + "username": "eda87756947b45119c603ff1041ebdba", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:17.887924+00:00", + "modified_at": "2024-04-21T14:13:17.887957+00:00", + "email": "1489a14ea33348da89916606e480129a@example.com", + "username": "1489a14ea33348da89916606e480129a", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.020546+00:00", + "modified_at": "2024-04-21T14:13:18.020572+00:00", + "email": "c80b1ccd70ab418b8fe72e258f49ad19@example.com", + "username": "c80b1ccd70ab418b8fe72e258f49ad19", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.154489+00:00", + "modified_at": "2024-04-21T14:13:18.154517+00:00", + "email": "75d88021270e47e58c9fcbcde1dc896c@example.com", + "username": "75d88021270e47e58c9fcbcde1dc896c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.196536+00:00", + "modified_at": "2024-04-21T14:13:18.196571+00:00", + "email": "d8eb48d58e054947936e44282982fafa@example.com", + "username": "d8eb48d58e054947936e44282982fafa", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.251929+00:00", + "modified_at": "2024-04-21T14:13:18.251961+00:00", + "email": "068c3fb3c15f4b0c9065dd13971c2589@example.com", + "username": "068c3fb3c15f4b0c9065dd13971c2589", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.300251+00:00", + "modified_at": "2024-04-21T14:13:18.300290+00:00", + "email": "2fd294b2e8e34e5e93f7882b7f2583df@example.com", + "username": "2fd294b2e8e34e5e93f7882b7f2583df", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.352085+00:00", + "modified_at": "2024-04-21T14:13:18.352115+00:00", + "email": "c390cf01ac014900938dbf805c535127@example.com", + "username": "c390cf01ac014900938dbf805c535127", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.397192+00:00", + "modified_at": "2024-04-21T14:13:18.397220+00:00", + "email": "04eb7e53b7444df28e28ec35064acd1e@example.com", + "username": "04eb7e53b7444df28e28ec35064acd1e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.440747+00:00", + "modified_at": "2024-04-21T14:13:18.440779+00:00", + "email": "ecd974191e5d4383a496b61a2e5d8229@example.com", + "username": "ecd974191e5d4383a496b61a2e5d8229", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.678237+00:00", + "modified_at": "2024-04-21T14:13:18.678260+00:00", + "email": "f4b68905fa574602a28813936708cfd6@example.com", + "username": "f4b68905fa574602a28813936708cfd6", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.721020+00:00", + "modified_at": "2024-04-21T14:13:18.721052+00:00", + "email": "12631bdab2cf43d085fb1f5ccf73bc8e@example.com", + "username": "12631bdab2cf43d085fb1f5ccf73bc8e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.775165+00:00", + "modified_at": "2024-04-21T14:13:18.775190+00:00", + "email": "465de770abcc45208133146e89b85be4@example.com", + "username": "465de770abcc45208133146e89b85be4", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.816796+00:00", + "modified_at": "2024-04-21T14:13:18.816821+00:00", + "email": "3530d3ac88a6458fab287138b5ad61e7@example.com", + "username": "3530d3ac88a6458fab287138b5ad61e7", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.858498+00:00", + "modified_at": "2024-04-21T14:13:18.858520+00:00", + "email": "3deac43eb4094e83b8dc35f6d82aee9c@example.com", + "username": "3deac43eb4094e83b8dc35f6d82aee9c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.902304+00:00", + "modified_at": "2024-04-21T14:13:18.902336+00:00", + "email": "bcbba7143184456f9252f1296dd8cd3e@example.com", + "username": "bcbba7143184456f9252f1296dd8cd3e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:18.944969+00:00", + "modified_at": "2024-04-21T14:13:18.945013+00:00", + "email": "117c383797364d0f8e6e245f2ac149e0@example.com", + "username": "117c383797364d0f8e6e245f2ac149e0", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.071574+00:00", + "modified_at": "2024-04-21T14:13:19.071600+00:00", + "email": "4b2c6adaccf447bcbfca57021817a390@example.com", + "username": "4b2c6adaccf447bcbfca57021817a390", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.200907+00:00", + "modified_at": "2024-04-21T14:13:19.200931+00:00", + "email": "8423959f69474cd58d16b61a0eb7679d@example.com", + "username": "8423959f69474cd58d16b61a0eb7679d", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.244344+00:00", + "modified_at": "2024-04-21T14:13:19.244377+00:00", + "email": "dc539989d3d84f0cabbe283a5d996106@example.com", + "username": "dc539989d3d84f0cabbe283a5d996106", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.286127+00:00", + "modified_at": "2024-04-21T14:13:19.286159+00:00", + "email": "2dcb7ccd71b146459ee2eb39067fbfbb@example.com", + "username": "2dcb7ccd71b146459ee2eb39067fbfbb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.328686+00:00", + "modified_at": "2024-04-21T14:13:19.328710+00:00", + "email": "26811c7f9b29492985d39f29de404a39@example.com", + "username": "26811c7f9b29492985d39f29de404a39", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.377852+00:00", + "modified_at": "2024-04-21T14:13:19.377875+00:00", + "email": "d73ea7ca83864305997b3d1681e9f8fd@example.com", + "username": "d73ea7ca83864305997b3d1681e9f8fd", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.422137+00:00", + "modified_at": "2024-04-21T14:13:19.422174+00:00", + "email": "7f51c9095f3f42b08d4ef58ae519acdf@example.com", + "username": "7f51c9095f3f42b08d4ef58ae519acdf", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.489467+00:00", + "modified_at": "2024-04-21T14:13:19.489489+00:00", + "email": "b3d1bc1ac4fd4812af76abf610fb850b@example.com", + "username": "b3d1bc1ac4fd4812af76abf610fb850b", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.727290+00:00", + "modified_at": "2024-04-21T14:13:19.727316+00:00", + "email": "e904e63c16384bf3bd94f1e56063e3b0@example.com", + "username": "e904e63c16384bf3bd94f1e56063e3b0", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.771807+00:00", + "modified_at": "2024-04-21T14:13:19.771840+00:00", + "email": "5d5a5ab819cd458dbbc99056cd6563dd@example.com", + "username": "5d5a5ab819cd458dbbc99056cd6563dd", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.828222+00:00", + "modified_at": "2024-04-21T14:13:19.828245+00:00", + "email": "cfd1197f4f224f2a946e7b8d2ec754eb@example.com", + "username": "cfd1197f4f224f2a946e7b8d2ec754eb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.874232+00:00", + "modified_at": "2024-04-21T14:13:19.874262+00:00", + "email": "9eb98ef83cfc414691c0be57988ba188@example.com", + "username": "9eb98ef83cfc414691c0be57988ba188", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.915821+00:00", + "modified_at": "2024-04-21T14:13:19.915848+00:00", + "email": "96897185a4014b709c64d67491ec19b7@example.com", + "username": "96897185a4014b709c64d67491ec19b7", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:19.958320+00:00", + "modified_at": "2024-04-21T14:13:19.958353+00:00", + "email": "40225176da3a48ecbec8e528ba31b2c5@example.com", + "username": "40225176da3a48ecbec8e528ba31b2c5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.000775+00:00", + "modified_at": "2024-04-21T14:13:20.000803+00:00", + "email": "09ac45ff9e134031a88e8e1dbafeec60@example.com", + "username": "09ac45ff9e134031a88e8e1dbafeec60", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.120004+00:00", + "modified_at": "2024-04-21T14:13:20.120048+00:00", + "email": "fbad40917c3a4afcb08a08d995673c6f@example.com", + "username": "fbad40917c3a4afcb08a08d995673c6f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.248878+00:00", + "modified_at": "2024-04-21T14:13:20.248901+00:00", + "email": "798f0223ae524d37a3f82b9b7c3a3815@example.com", + "username": "798f0223ae524d37a3f82b9b7c3a3815", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.320188+00:00", + "modified_at": "2024-04-21T14:13:20.320224+00:00", + "email": "6a608424353c45a28b1012621e12f4bc@example.com", + "username": "6a608424353c45a28b1012621e12f4bc", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.362598+00:00", + "modified_at": "2024-04-21T14:13:20.362636+00:00", + "email": "cd28a047c75c4080b141ebd6466bb464@example.com", + "username": "cd28a047c75c4080b141ebd6466bb464", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.404931+00:00", + "modified_at": "2024-04-21T14:13:20.404965+00:00", + "email": "f60f24ea56584562b85d5f8034a927eb@example.com", + "username": "f60f24ea56584562b85d5f8034a927eb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.447868+00:00", + "modified_at": "2024-04-21T14:13:20.447902+00:00", + "email": "bbc76f689aa042d78d73cb32677b06ff@example.com", + "username": "bbc76f689aa042d78d73cb32677b06ff", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.490023+00:00", + "modified_at": "2024-04-21T14:13:20.490050+00:00", + "email": "c73240f86cec41af8075a5859fccd464@example.com", + "username": "c73240f86cec41af8075a5859fccd464", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.532783+00:00", + "modified_at": "2024-04-21T14:13:20.532821+00:00", + "email": "2d6f870030394956a0a07c1d548326e5@example.com", + "username": "2d6f870030394956a0a07c1d548326e5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.775296+00:00", + "modified_at": "2024-04-21T14:13:20.775321+00:00", + "email": "b943ca03f7804af89c30a6544b294b66@example.com", + "username": "b943ca03f7804af89c30a6544b294b66", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.815813+00:00", + "modified_at": "2024-04-21T14:13:20.815840+00:00", + "email": "fcb7975b47a44ca6afc2fa50ae62c302@example.com", + "username": "fcb7975b47a44ca6afc2fa50ae62c302", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.860674+00:00", + "modified_at": "2024-04-21T14:13:20.860699+00:00", + "email": "c0f6c79475d54988a418bb5abcc71c60@example.com", + "username": "c0f6c79475d54988a418bb5abcc71c60", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.903271+00:00", + "modified_at": "2024-04-21T14:13:20.903322+00:00", + "email": "f54d9b3222d74a578de7704a99f5e9d0@example.com", + "username": "f54d9b3222d74a578de7704a99f5e9d0", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:20.958688+00:00", + "modified_at": "2024-04-21T14:13:20.958752+00:00", + "email": "602a5d4afac84fe094f446daec96a3c6@example.com", + "username": "602a5d4afac84fe094f446daec96a3c6", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.002757+00:00", + "modified_at": "2024-04-21T14:13:21.002794+00:00", + "email": "44eb9a9d322b49be8814c4ec837497fb@example.com", + "username": "44eb9a9d322b49be8814c4ec837497fb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.067203+00:00", + "modified_at": "2024-04-21T14:13:21.067225+00:00", + "email": "1240a1ff19544bc58bf5b0b9532aca14@example.com", + "username": "1240a1ff19544bc58bf5b0b9532aca14", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.166345+00:00", + "modified_at": "2024-04-21T14:13:21.166370+00:00", + "email": "a6e242820805406fb75e88d0683d71dd@example.com", + "username": "a6e242820805406fb75e88d0683d71dd", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.298622+00:00", + "modified_at": "2024-04-21T14:13:21.298647+00:00", + "email": "f5e73c0ea19a4973903fa867d41cc293@example.com", + "username": "f5e73c0ea19a4973903fa867d41cc293", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.342470+00:00", + "modified_at": "2024-04-21T14:13:21.342627+00:00", + "email": "32b53142a15c4a1ba05e7fe12f1bb59a@example.com", + "username": "32b53142a15c4a1ba05e7fe12f1bb59a", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.427208+00:00", + "modified_at": "2024-04-21T14:13:21.427231+00:00", + "email": "fce977034da14827ab548115b18ae7cc@example.com", + "username": "fce977034da14827ab548115b18ae7cc", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.469003+00:00", + "modified_at": "2024-04-21T14:13:21.469027+00:00", + "email": "e621f5ec579c4ccb8e26b4f6df2d6e49@example.com", + "username": "e621f5ec579c4ccb8e26b4f6df2d6e49", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.519323+00:00", + "modified_at": "2024-04-21T14:13:21.519342+00:00", + "email": "492de943644440e3b34c16a7d6fc3824@example.com", + "username": "492de943644440e3b34c16a7d6fc3824", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.562778+00:00", + "modified_at": "2024-04-21T14:13:21.562805+00:00", + "email": "4568d5fca9cf42f785d9982f20b2b51e@example.com", + "username": "4568d5fca9cf42f785d9982f20b2b51e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.824497+00:00", + "modified_at": "2024-04-21T14:13:21.824520+00:00", + "email": "7c1fd141b7104c099970eafbbf07f613@example.com", + "username": "7c1fd141b7104c099970eafbbf07f613", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.898849+00:00", + "modified_at": "2024-04-21T14:13:21.898879+00:00", + "email": "9d2cf68933cd49d88de11f64c1aeedc1@example.com", + "username": "9d2cf68933cd49d88de11f64c1aeedc1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.942339+00:00", + "modified_at": "2024-04-21T14:13:21.942366+00:00", + "email": "bf83a426b89942348f2d61739c89de6f@example.com", + "username": "bf83a426b89942348f2d61739c89de6f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:21.984329+00:00", + "modified_at": "2024-04-21T14:13:21.984369+00:00", + "email": "fa0e4b81270b4574b132dc0e3d8e5971@example.com", + "username": "fa0e4b81270b4574b132dc0e3d8e5971", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.026639+00:00", + "modified_at": "2024-04-21T14:13:22.026670+00:00", + "email": "63ccbb1878744d83b7b557fa741e50b5@example.com", + "username": "63ccbb1878744d83b7b557fa741e50b5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.069383+00:00", + "modified_at": "2024-04-21T14:13:22.069406+00:00", + "email": "acb5c26186464c8f98d7c0903706caaf@example.com", + "username": "acb5c26186464c8f98d7c0903706caaf", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.112184+00:00", + "modified_at": "2024-04-21T14:13:22.112216+00:00", + "email": "4551d67d848e4ffc900b3d6615170428@example.com", + "username": "4551d67d848e4ffc900b3d6615170428", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.216312+00:00", + "modified_at": "2024-04-21T14:13:22.216335+00:00", + "email": "5d82d38882b94d9bb108e20f9fefbe10@example.com", + "username": "5d82d38882b94d9bb108e20f9fefbe10", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.347762+00:00", + "modified_at": "2024-04-21T14:13:22.347787+00:00", + "email": "779b35d28c7945b5a0017d8149bd1ed1@example.com", + "username": "779b35d28c7945b5a0017d8149bd1ed1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.392395+00:00", + "modified_at": "2024-04-21T14:13:22.392425+00:00", + "email": "cacf487045c14208882efb3708bdcc84@example.com", + "username": "cacf487045c14208882efb3708bdcc84", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.436266+00:00", + "modified_at": "2024-04-21T14:13:22.436299+00:00", + "email": "f4fdc27737244f448b9d90ff60c55d11@example.com", + "username": "f4fdc27737244f448b9d90ff60c55d11", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.478019+00:00", + "modified_at": "2024-04-21T14:13:22.478050+00:00", + "email": "cea348942bc642ada4066034e3887de2@example.com", + "username": "cea348942bc642ada4066034e3887de2", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.521652+00:00", + "modified_at": "2024-04-21T14:13:22.521684+00:00", + "email": "34a6f1d85ba3458d9b9901e6ad32894f@example.com", + "username": "34a6f1d85ba3458d9b9901e6ad32894f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.564653+00:00", + "modified_at": "2024-04-21T14:13:22.564685+00:00", + "email": "9358419848c44e06b060443bc43d9758@example.com", + "username": "9358419848c44e06b060443bc43d9758", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.606888+00:00", + "modified_at": "2024-04-21T14:13:22.606919+00:00", + "email": "34b6770e3d2c45cf9bcebd9d66d47629@example.com", + "username": "34b6770e3d2c45cf9bcebd9d66d47629", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.648312+00:00", + "modified_at": "2024-04-21T14:13:22.648336+00:00", + "email": "12615190bdfc476fbd439f28ce1c1120@example.com", + "username": "12615190bdfc476fbd439f28ce1c1120", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.874117+00:00", + "modified_at": "2024-04-21T14:13:22.874142+00:00", + "email": "8745e1c5b53a4f4f86a7499e2821e35b@example.com", + "username": "8745e1c5b53a4f4f86a7499e2821e35b", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.917628+00:00", + "modified_at": "2024-04-21T14:13:22.917660+00:00", + "email": "cf4b9ea9169d4f8db42bf1bf54b6869c@example.com", + "username": "cf4b9ea9169d4f8db42bf1bf54b6869c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:22.959963+00:00", + "modified_at": "2024-04-21T14:13:22.959995+00:00", + "email": "ef7cedd1f48c45f9b36f8afb05e023ea@example.com", + "username": "ef7cedd1f48c45f9b36f8afb05e023ea", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.002681+00:00", + "modified_at": "2024-04-21T14:13:23.002713+00:00", + "email": "1d3af72ce8cf45db8a02dc015b8b585f@example.com", + "username": "1d3af72ce8cf45db8a02dc015b8b585f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.046503+00:00", + "modified_at": "2024-04-21T14:13:23.046534+00:00", + "email": "61ef44f37c8f437cafec6eb5a6e8b364@example.com", + "username": "61ef44f37c8f437cafec6eb5a6e8b364", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.106110+00:00", + "modified_at": "2024-04-21T14:13:23.106143+00:00", + "email": "e1ee84245cb84b91b3966193619268fb@example.com", + "username": "e1ee84245cb84b91b3966193619268fb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.151923+00:00", + "modified_at": "2024-04-21T14:13:23.151955+00:00", + "email": "20300e338d744f9aa5b114bbde28a796@example.com", + "username": "20300e338d744f9aa5b114bbde28a796", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.264936+00:00", + "modified_at": "2024-04-21T14:13:23.264963+00:00", + "email": "21bf6d9062af460d96525d777f7b18a9@example.com", + "username": "21bf6d9062af460d96525d777f7b18a9", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.395233+00:00", + "modified_at": "2024-04-21T14:13:23.395259+00:00", + "email": "e5f712cf8e544ca8b8027432b9de377d@example.com", + "username": "e5f712cf8e544ca8b8027432b9de377d", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.447205+00:00", + "modified_at": "2024-04-21T14:13:23.447237+00:00", + "email": "dac893d66a624350a7297253216b20ef@example.com", + "username": "dac893d66a624350a7297253216b20ef", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.489157+00:00", + "modified_at": "2024-04-21T14:13:23.489190+00:00", + "email": "28ebbbefdc0f44bba131e9707f7c6f8e@example.com", + "username": "28ebbbefdc0f44bba131e9707f7c6f8e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.532296+00:00", + "modified_at": "2024-04-21T14:13:23.532327+00:00", + "email": "e989a724455345a4842a887624114f2b@example.com", + "username": "e989a724455345a4842a887624114f2b", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.607816+00:00", + "modified_at": "2024-04-21T14:13:23.607849+00:00", + "email": "fb5b633e96644d6ba8520f483652d4f1@example.com", + "username": "fb5b633e96644d6ba8520f483652d4f1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.650043+00:00", + "modified_at": "2024-04-21T14:13:23.650075+00:00", + "email": "ed94a6763aa3424c82a43aca38bf53fa@example.com", + "username": "ed94a6763aa3424c82a43aca38bf53fa", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.695205+00:00", + "modified_at": "2024-04-21T14:13:23.695232+00:00", + "email": "e89f4fe6413b44b29dbc5cb27dde44ee@example.com", + "username": "e89f4fe6413b44b29dbc5cb27dde44ee", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:23.961803+00:00", + "modified_at": "2024-04-21T14:13:23.961835+00:00", + "email": "94ae3caa587b4c50af21818776a0036c@example.com", + "username": "94ae3caa587b4c50af21818776a0036c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.021788+00:00", + "modified_at": "2024-04-21T14:13:24.021813+00:00", + "email": "2391c00a44ff492688b02b1b44241be8@example.com", + "username": "2391c00a44ff492688b02b1b44241be8", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.069560+00:00", + "modified_at": "2024-04-21T14:13:24.069592+00:00", + "email": "d972dc58c11042a1951c46114c7b86e5@example.com", + "username": "d972dc58c11042a1951c46114c7b86e5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.114631+00:00", + "modified_at": "2024-04-21T14:13:24.114664+00:00", + "email": "14e739883b7a4e18ab1368b6404b237a@example.com", + "username": "14e739883b7a4e18ab1368b6404b237a", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.158341+00:00", + "modified_at": "2024-04-21T14:13:24.158375+00:00", + "email": "eb0a0504299b44c3b7be0198c0e5e7c8@example.com", + "username": "eb0a0504299b44c3b7be0198c0e5e7c8", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.201720+00:00", + "modified_at": "2024-04-21T14:13:24.201753+00:00", + "email": "214adbfb60c34667a2d63f0679cdfed0@example.com", + "username": "214adbfb60c34667a2d63f0679cdfed0", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.312298+00:00", + "modified_at": "2024-04-21T14:13:24.312321+00:00", + "email": "85726e622bb344728b5fc1ae4b281868@example.com", + "username": "85726e622bb344728b5fc1ae4b281868", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.359115+00:00", + "modified_at": "2024-04-21T14:13:24.359135+00:00", + "email": "a729bf19297147719f7df2d0615e7853@example.com", + "username": "a729bf19297147719f7df2d0615e7853", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.444946+00:00", + "modified_at": "2024-04-21T14:13:24.444968+00:00", + "email": "0834ca316eab4279bbea084e0ca5680f@example.com", + "username": "0834ca316eab4279bbea084e0ca5680f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.528319+00:00", + "modified_at": "2024-04-21T14:13:24.528351+00:00", + "email": "baf15781cbd64e63809fd1005fa616c7@example.com", + "username": "baf15781cbd64e63809fd1005fa616c7", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.572265+00:00", + "modified_at": "2024-04-21T14:13:24.572299+00:00", + "email": "cffd613b682f4666a893181c005e9f62@example.com", + "username": "cffd613b682f4666a893181c005e9f62", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.615596+00:00", + "modified_at": "2024-04-21T14:13:24.615629+00:00", + "email": "dbf3b875558d4d19b025fee89b84e8a6@example.com", + "username": "dbf3b875558d4d19b025fee89b84e8a6", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.658436+00:00", + "modified_at": "2024-04-21T14:13:24.658469+00:00", + "email": "a6743309581b401daf745def49475719@example.com", + "username": "a6743309581b401daf745def49475719", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.700601+00:00", + "modified_at": "2024-04-21T14:13:24.700633+00:00", + "email": "ec6667b7fcf4401eb7e61b8423225445@example.com", + "username": "ec6667b7fcf4401eb7e61b8423225445", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.743186+00:00", + "modified_at": "2024-04-21T14:13:24.743220+00:00", + "email": "262562ec7db3480eaa37e075571e3d47@example.com", + "username": "262562ec7db3480eaa37e075571e3d47", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:24.970505+00:00", + "modified_at": "2024-04-21T14:13:24.970528+00:00", + "email": "5f8384f6abbd4154b32dce1f41fec14f@example.com", + "username": "5f8384f6abbd4154b32dce1f41fec14f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.014880+00:00", + "modified_at": "2024-04-21T14:13:25.014922+00:00", + "email": "96aa25fe639442cd80a7849404bedf7e@example.com", + "username": "96aa25fe639442cd80a7849404bedf7e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.057321+00:00", + "modified_at": "2024-04-21T14:13:25.057354+00:00", + "email": "bcc74dcda17144b48ea87cef8082bc65@example.com", + "username": "bcc74dcda17144b48ea87cef8082bc65", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.100660+00:00", + "modified_at": "2024-04-21T14:13:25.100694+00:00", + "email": "b5ed3aec3cca4f2593ee7dafde130417@example.com", + "username": "b5ed3aec3cca4f2593ee7dafde130417", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.143619+00:00", + "modified_at": "2024-04-21T14:13:25.143651+00:00", + "email": "6a7295d057a14ab9ad778ed6493cf9c3@example.com", + "username": "6a7295d057a14ab9ad778ed6493cf9c3", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.184530+00:00", + "modified_at": "2024-04-21T14:13:25.184563+00:00", + "email": "65831e5490664227960b0777d8001eb8@example.com", + "username": "65831e5490664227960b0777d8001eb8", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.266293+00:00", + "modified_at": "2024-04-21T14:13:25.266313+00:00", + "email": "ac44340aa05c45bbba400c5272af8a8e@example.com", + "username": "ac44340aa05c45bbba400c5272af8a8e", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.689130+00:00", + "modified_at": "2024-04-21T14:13:25.689154+00:00", + "email": "b55368ba77ba437687cbf6b6c7d916f7@example.com", + "username": "b55368ba77ba437687cbf6b6c7d916f7", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.729856+00:00", + "modified_at": "2024-04-21T14:13:25.729889+00:00", + "email": "f10fa2896e3c4d7eb7e80ac768d6bc12@example.com", + "username": "f10fa2896e3c4d7eb7e80ac768d6bc12", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:25.774349+00:00", + "modified_at": "2024-04-21T14:13:25.774386+00:00", + "email": "91d301a81dc8476497ab64af59e48d25@example.com", + "username": "91d301a81dc8476497ab64af59e48d25", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.218833+00:00", + "modified_at": "2024-04-21T14:13:26.218858+00:00", + "email": "ae34785d24744bc98d97a9f317957f2b@example.com", + "username": "ae34785d24744bc98d97a9f317957f2b", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.259622+00:00", + "modified_at": "2024-04-21T14:13:26.259655+00:00", + "email": "ba3f162a26c54a11abcc191c197f42b2@example.com", + "username": "ba3f162a26c54a11abcc191c197f42b2", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.306689+00:00", + "modified_at": "2024-04-21T14:13:26.306722+00:00", + "email": "1dbdbc5b51014d518ae1629e0bdd6349@example.com", + "username": "1dbdbc5b51014d518ae1629e0bdd6349", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.411603+00:00", + "modified_at": "2024-04-21T14:13:26.411626+00:00", + "email": "d911065e5a4e49409b11cde7bc2cad95@example.com", + "username": "d911065e5a4e49409b11cde7bc2cad95", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.541830+00:00", + "modified_at": "2024-04-21T14:13:26.541854+00:00", + "email": "e66825aeb77746c990bf8d0a48c354d5@example.com", + "username": "e66825aeb77746c990bf8d0a48c354d5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.592247+00:00", + "modified_at": "2024-04-21T14:13:26.592279+00:00", + "email": "555e08f405474b5ebba17106d1dec86c@example.com", + "username": "555e08f405474b5ebba17106d1dec86c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.635951+00:00", + "modified_at": "2024-04-21T14:13:26.635984+00:00", + "email": "5d1283d46e8e4c649649422b0f0d5e7a@example.com", + "username": "5d1283d46e8e4c649649422b0f0d5e7a", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.677911+00:00", + "modified_at": "2024-04-21T14:13:26.677943+00:00", + "email": "b4a1459edc954d33ba3aba84e8ad2c2f@example.com", + "username": "b4a1459edc954d33ba3aba84e8ad2c2f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.721255+00:00", + "modified_at": "2024-04-21T14:13:26.721295+00:00", + "email": "a466540fcbc04ee9a81175ca54de4ec0@example.com", + "username": "a466540fcbc04ee9a81175ca54de4ec0", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.764037+00:00", + "modified_at": "2024-04-21T14:13:26.764069+00:00", + "email": "cad2e4e51a4a4ddf9c86e7769716ab8c@example.com", + "username": "cad2e4e51a4a4ddf9c86e7769716ab8c", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:26.806394+00:00", + "modified_at": "2024-04-21T14:13:26.806425+00:00", + "email": "b29b185463044285ac03507dc338c8d4@example.com", + "username": "b29b185463044285ac03507dc338c8d4", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.066964+00:00", + "modified_at": "2024-04-21T14:13:27.066989+00:00", + "email": "a3000d4fcddb49bb8ba047437040ed04@example.com", + "username": "a3000d4fcddb49bb8ba047437040ed04", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.107813+00:00", + "modified_at": "2024-04-21T14:13:27.107839+00:00", + "email": "f9e7602f60654d2da4f219b52c3f421f@example.com", + "username": "f9e7602f60654d2da4f219b52c3f421f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.150296+00:00", + "modified_at": "2024-04-21T14:13:27.150330+00:00", + "email": "efa69f304dd344c3873f4635931134c7@example.com", + "username": "efa69f304dd344c3873f4635931134c7", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.192887+00:00", + "modified_at": "2024-04-21T14:13:27.192918+00:00", + "email": "889c24c9a88a4826b392cd8b39962bf1@example.com", + "username": "889c24c9a88a4826b392cd8b39962bf1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.237204+00:00", + "modified_at": "2024-04-21T14:13:27.237236+00:00", + "email": "2d448062237345869fbe24f870d10098@example.com", + "username": "2d448062237345869fbe24f870d10098", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.301702+00:00", + "modified_at": "2024-04-21T14:13:27.301918+00:00", + "email": "a1b06e40d414451bbdd74b4b5343c35f@example.com", + "username": "a1b06e40d414451bbdd74b4b5343c35f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.356352+00:00", + "modified_at": "2024-04-21T14:13:27.356382+00:00", + "email": "ec9308cd44614e2c9bf692d79690f6ad@example.com", + "username": "ec9308cd44614e2c9bf692d79690f6ad", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.461975+00:00", + "modified_at": "2024-04-21T14:13:27.462008+00:00", + "email": "f4be9c1108d4489dadb79857055722c3@example.com", + "username": "f4be9c1108d4489dadb79857055722c3", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.589522+00:00", + "modified_at": "2024-04-21T14:13:27.589554+00:00", + "email": "ed87cb865931448e977130564fe5f7d6@example.com", + "username": "ed87cb865931448e977130564fe5f7d6", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.631802+00:00", + "modified_at": "2024-04-21T14:13:27.631830+00:00", + "email": "41bfffa2644541de8fa816e6925a2beb@example.com", + "username": "41bfffa2644541de8fa816e6925a2beb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.674633+00:00", + "modified_at": "2024-04-21T14:13:27.674665+00:00", + "email": "51ec951853624ab9866af2b8b2430e94@example.com", + "username": "51ec951853624ab9866af2b8b2430e94", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.719945+00:00", + "modified_at": "2024-04-21T14:13:27.719972+00:00", + "email": "aadca7d3b84b4165b5559432efb7402d@example.com", + "username": "aadca7d3b84b4165b5559432efb7402d", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.796928+00:00", + "modified_at": "2024-04-21T14:13:27.796959+00:00", + "email": "452872dbabd04a6da602d27eaa5922aa@example.com", + "username": "452872dbabd04a6da602d27eaa5922aa", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.838327+00:00", + "modified_at": "2024-04-21T14:13:27.838359+00:00", + "email": "331415abe6c744ee857126c527a1bfa1@example.com", + "username": "331415abe6c744ee857126c527a1bfa1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:27.880249+00:00", + "modified_at": "2024-04-21T14:13:27.880272+00:00", + "email": "3eb02fa6a5f44dcbbd68a5fa22e4d5f6@example.com", + "username": "3eb02fa6a5f44dcbbd68a5fa22e4d5f6", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.115687+00:00", + "modified_at": "2024-04-21T14:13:28.115709+00:00", + "email": "5d4467ac56c04f79ba53b3e7348496a1@example.com", + "username": "5d4467ac56c04f79ba53b3e7348496a1", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.156805+00:00", + "modified_at": "2024-04-21T14:13:28.156838+00:00", + "email": "ed0353e7edc849248268c780729aaf51@example.com", + "username": "ed0353e7edc849248268c780729aaf51", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.215108+00:00", + "modified_at": "2024-04-21T14:13:28.215128+00:00", + "email": "f144124f9e654b56a61883ae3fa31eb5@example.com", + "username": "f144124f9e654b56a61883ae3fa31eb5", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.257909+00:00", + "modified_at": "2024-04-21T14:13:28.257941+00:00", + "email": "7381bd86bb574782a43ff041aa0d6b60@example.com", + "username": "7381bd86bb574782a43ff041aa0d6b60", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.300358+00:00", + "modified_at": "2024-04-21T14:13:28.300391+00:00", + "email": "4766830ec0c5493288027c7f9bdb782a@example.com", + "username": "4766830ec0c5493288027c7f9bdb782a", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.343251+00:00", + "modified_at": "2024-04-21T14:13:28.343283+00:00", + "email": "24a64bd918b5492e97eef6ed95e8947d@example.com", + "username": "24a64bd918b5492e97eef6ed95e8947d", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.385241+00:00", + "modified_at": "2024-04-21T14:13:28.385261+00:00", + "email": "2bfb21b96a4c4ce9bc5865fc706fef86@example.com", + "username": "2bfb21b96a4c4ce9bc5865fc706fef86", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.507756+00:00", + "modified_at": "2024-04-21T14:13:28.507782+00:00", + "email": "8d3a51fa474b42e793778b7dabfe01a7@example.com", + "username": "8d3a51fa474b42e793778b7dabfe01a7", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.552451+00:00", + "modified_at": "2024-04-21T14:13:28.552472+00:00", + "email": "8903676616194f2585130f9e9f1ae297@example.com", + "username": "8903676616194f2585130f9e9f1ae297", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.638002+00:00", + "modified_at": "2024-04-21T14:13:28.638025+00:00", + "email": "b950d91862a84b0095beb11a56a8dc11@example.com", + "username": "b950d91862a84b0095beb11a56a8dc11", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.711335+00:00", + "modified_at": "2024-04-21T14:13:28.711369+00:00", + "email": "179d11327a074f98b500512c104c8a88@example.com", + "username": "179d11327a074f98b500512c104c8a88", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.754984+00:00", + "modified_at": "2024-04-21T14:13:28.755016+00:00", + "email": "2158334ab6834fb2af133105d5f024e2@example.com", + "username": "2158334ab6834fb2af133105d5f024e2", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.798163+00:00", + "modified_at": "2024-04-21T14:13:28.798196+00:00", + "email": "2238969df5e7472c93ac26b1581fe054@example.com", + "username": "2238969df5e7472c93ac26b1581fe054", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.842318+00:00", + "modified_at": "2024-04-21T14:13:28.842351+00:00", + "email": "c4c5dbd47eea4268929ba9f67379a056@example.com", + "username": "c4c5dbd47eea4268929ba9f67379a056", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.883999+00:00", + "modified_at": "2024-04-21T14:13:28.884025+00:00", + "email": "2d471d8f6c5f430cbc09cab1540ddf5d@example.com", + "username": "2d471d8f6c5f430cbc09cab1540ddf5d", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:28.931023+00:00", + "modified_at": "2024-04-21T14:13:28.931056+00:00", + "email": "74ba0c6bca134bc3a963d53ff0e33e92@example.com", + "username": "74ba0c6bca134bc3a963d53ff0e33e92", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.162341+00:00", + "modified_at": "2024-04-21T14:13:29.162365+00:00", + "email": "f01c7e45364d4019b18eb8e3e2ae6e11@example.com", + "username": "f01c7e45364d4019b18eb8e3e2ae6e11", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.201928+00:00", + "modified_at": "2024-04-21T14:13:29.201951+00:00", + "email": "6a24fc5d01a245d1bc316404ac68b728@example.com", + "username": "6a24fc5d01a245d1bc316404ac68b728", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.242815+00:00", + "modified_at": "2024-04-21T14:13:29.242838+00:00", + "email": "61354f419ef740419b65913203c24271@example.com", + "username": "61354f419ef740419b65913203c24271", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.282810+00:00", + "modified_at": "2024-04-21T14:13:29.282835+00:00", + "email": "15d4149e117044a7b8825901c67a43df@example.com", + "username": "15d4149e117044a7b8825901c67a43df", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.324062+00:00", + "modified_at": "2024-04-21T14:13:29.324089+00:00", + "email": "9a98cf8cf90a46ea922e4203f219b6ea@example.com", + "username": "9a98cf8cf90a46ea922e4203f219b6ea", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.366632+00:00", + "modified_at": "2024-04-21T14:13:29.366668+00:00", + "email": "f09ca9388bc049ddb80c5fdcd3b5a554@example.com", + "username": "f09ca9388bc049ddb80c5fdcd3b5a554", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.410573+00:00", + "modified_at": "2024-04-21T14:13:29.410603+00:00", + "email": "3ddad4bb46394413967a6625bd3ca57d@example.com", + "username": "3ddad4bb46394413967a6625bd3ca57d", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.463139+00:00", + "modified_at": "2024-04-21T14:13:29.463163+00:00", + "email": "42fef4f9b7e841df99e3f07811eb8498@example.com", + "username": "42fef4f9b7e841df99e3f07811eb8498", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.556195+00:00", + "modified_at": "2024-04-21T14:13:29.556223+00:00", + "email": "85ed485cc1644931b7ace9bebe8f0fb8@example.com", + "username": "85ed485cc1644931b7ace9bebe8f0fb8", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.704086+00:00", + "modified_at": "2024-04-21T14:13:29.704113+00:00", + "email": "7f8db7a1ff754f8a8ef5366f94546780@example.com", + "username": "7f8db7a1ff754f8a8ef5366f94546780", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.752432+00:00", + "modified_at": "2024-04-21T14:13:29.752506+00:00", + "email": "874101af30374c01b027a447e812bf40@example.com", + "username": "874101af30374c01b027a447e812bf40", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.813821+00:00", + "modified_at": "2024-04-21T14:13:29.813842+00:00", + "email": "ad48ee434c9c46d3bf995926aadac8db@example.com", + "username": "ad48ee434c9c46d3bf995926aadac8db", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.854938+00:00", + "modified_at": "2024-04-21T14:13:29.854966+00:00", + "email": "d8bf1c33db684222845b9c5fce124fdb@example.com", + "username": "d8bf1c33db684222845b9c5fce124fdb", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.907354+00:00", + "modified_at": "2024-04-21T14:13:29.907378+00:00", + "email": "9bbf22b118454965a3da016af3a71344@example.com", + "username": "9bbf22b118454965a3da016af3a71344", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + }, + { + "created_at": "2024-04-21T14:13:29.959363+00:00", + "modified_at": "2024-04-21T14:13:29.959399+00:00", + "email": "4b0426f9a0ec4bea97d29ed4b25b583f@example.com", + "username": "4b0426f9a0ec4bea97d29ed4b25b583f", + "password": "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC", + "is_superuser": false + } +] diff --git a/{{ cookiecutter.project_slug }}/generate_dummy_data.py b/{{ cookiecutter.project_slug }}/generate_dummy_data.py new file mode 100644 index 0000000..9d57b45 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/generate_dummy_data.py @@ -0,0 +1,73 @@ +import random +import string +import time +import uuid +from typing import Type + +from tortoise import Tortoise, run_async, Model + +from service.config import TORTOISE_ORM +from service.database.models import User + +CHARS = string.ascii_letters + string.digits +user_count = 205 +journal_count = 27 +form_count = 2 +form_input_count = 5 +option_count = 8 + + +def rand_str(k: int = 10) -> str: + return "".join(random.choices(CHARS, k=k)) + + +async def get_model_next_id(model: Type[Model]) -> int: + max_uid = await model.all().order_by("-id").first() + if max_uid is None: + max_uid = 0 + else: + max_uid = max_uid.id + return max_uid + 1 + + +async def do_import(): + await Tortoise.init(TORTOISE_ORM) + _p = "$2b$12$3k.eYVcZxKRbSpRaz/R5luVxI0QI.CRiANGE8LINDGU6El9jYQxgC" + usernames = [uuid.uuid4().hex for _ in range(user_count)] + u_id = await get_model_next_id(User) + users = [ + User( + id=u_id + i, + username=un, + email=f"{un}@{{ cookiecutter.project_slug }}.com", + password=_p, + ) + for i, un in enumerate(usernames) + ] + ts = time.time() + await User.bulk_create(users) + print(f"User: {len(users)} rows in {time.time() - ts:.5f}s)") + + uc = len(users) + print( + "Data generation finished!\n" + f"Total of {sum((uc, ))} objects created!\n" + f"Generated {uc} Users" + ) + toc = await User.all().count() + print( + "Total elements in tables:\n" + f"Users: {toc}\n" + f"Total: {toc}" + ) + + +def main(): + start_ts = time.time() + run_async(do_import()) + end_ts = time.time() + print(f"Data generated in {end_ts - start_ts:.3f}s") + + +if __name__ == "__main__": + main() diff --git a/{{ cookiecutter.project_slug }}/import_fixtures.py b/{{ cookiecutter.project_slug }}/import_fixtures.py new file mode 100644 index 0000000..84f9e9b --- /dev/null +++ b/{{ cookiecutter.project_slug }}/import_fixtures.py @@ -0,0 +1,42 @@ +import os.path +import time +from typing import Sequence, Type + +import orjson +from tortoise import Model, Tortoise, run_async +from tortoise.transactions import atomic + +from service.config import TORTOISE_ORM +from service.database.models import User + +models: Sequence[Type[Model]] = (User, ) + + +@atomic() +async def import_data(): + for filename, model in ((model._meta.db_table, model) for model in models): + ts = time.time() + _fn = f"fixtures/{filename}.json" + if not os.path.exists(_fn): + continue + with open(_fn, "r") as f: + data = orjson.loads(f.read()) + await model.bulk_create((model(**row) for row in data), ignore_conflicts=True) + te = time.time() + print(f"{model.__name__} imported ({len(data)} rows in {te - ts:.5f}s)") + + +async def do_import(): + await Tortoise.init(TORTOISE_ORM) + await import_data() + + +def main(): + start_ts = time.time() + run_async(do_import()) + end_ts = time.time() + print(f"Import completed in {end_ts - start_ts:.3f}s") + + +if __name__ == "__main__": + main() diff --git a/{{ cookiecutter.project_slug }}/migrations/service/0_20240422103838_init.py b/{{ cookiecutter.project_slug }}/migrations/service/0_20240422103838_init.py new file mode 100644 index 0000000..06ca485 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/migrations/service/0_20240422103838_init.py @@ -0,0 +1,26 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "user" ( + "id" BIGSERIAL NOT NULL PRIMARY KEY, + "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "modified_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "email" VARCHAR(255) NOT NULL UNIQUE, + "username" VARCHAR(32) NOT NULL UNIQUE, + "password" VARCHAR(256) NOT NULL, + "is_superuser" BOOL NOT NULL DEFAULT False +); +COMMENT ON COLUMN "user"."is_superuser" IS 'Is user a SuperUser?'; +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(100) NOT NULL, + "content" JSONB NOT NULL +);""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ diff --git a/{{ cookiecutter.project_slug }}/pyproject.toml b/{{ cookiecutter.project_slug }}/pyproject.toml new file mode 100644 index 0000000..02228ff --- /dev/null +++ b/{{ cookiecutter.project_slug }}/pyproject.toml @@ -0,0 +1,36 @@ +[tool.black] +line-length = 120 +target-version = ['py311'] +include = '\.pyi?$' +extend-exclude = '''( + migrations/* + | .git/* + | media/* +)''' +workers = 4 + + +[tool.isort] +profile = "black" +line_length = 120 +skip = ["migrations", "env", "venv", ".venv", ".git", "media"] + + +[tool.aerich] +tortoise_orm = "service.config.TORTOISE_ORM" +location = "./migrations" +src_folder = "./." + +[tool.mypy] +python_version = "3.11" +exclude = [ + '^\.?venv/', + 'migrations/' +] +plugins = ["pydantic.mypy"] +warn_unused_configs = true +disallow_untyped_defs = true +implicit_optional = true +warn_redundant_casts = true +warn_no_return = false +ignore_missing_imports = false diff --git a/{{ cookiecutter.project_slug }}/requirements/base.txt b/{{ cookiecutter.project_slug }}/requirements/base.txt new file mode 100644 index 0000000..460cb5d --- /dev/null +++ b/{{ cookiecutter.project_slug }}/requirements/base.txt @@ -0,0 +1,26 @@ +fastapi==0.115.5 # https://github.com/tiangolo/fastapi +uvicorn==0.32.1 # https://pypi.org/project/uvicorn/ + +pydantic[email]==2.10.2 # https://github.com/pydantic/pydantic +pydantic-settings[yaml]==2.6.1 # https://github.com/pydantic/pydantic-settings/ + +tortoise-orm[accel,asyncpg]==0.22.1 # https://pypi.org/project/tortoise-orm/ +aerich==0.7.2 # https://pypi.org/project/aerich/ + +setech==1.4.2 # https://pypi.org/project/setech/ + +python-multipart==0.0.18 # https://pypi.org/project/python-multipart/ +email-validator==2.2.0 # https://pypi.org/project/email-validator/ + + +tenacity==9.0.0 # https://pypi.org/project/tenacity/ +pydantic==2.10.2 # https://pypi.org/project/pydantic/ +#emails==0.6 # https://pypi.org/project/emails/ + +python-jose[cryptography]==3.3 # https://pypi.org/project/python-jose/ +passlib[bcrypt]==1.7.4 # https://pypi.org/project/passlib/ +bcrypt==4.2.1 # https://pypi.org/project/bcrypt/ +# Pin bcrypt until passlib supports the latest +pydantic-settings==2.6.1 # https://pypi.org/project/pydantic-settings/ + +asyncio==3.4.3 diff --git a/{{ cookiecutter.project_slug }}/requirements/local.txt b/{{ cookiecutter.project_slug }}/requirements/local.txt new file mode 100644 index 0000000..cf7beb7 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/requirements/local.txt @@ -0,0 +1,25 @@ +-r base.txt + +uvicorn[standard]==0.32.1 # https://pypi.org/project/uvicorn/ + +black==24.10.0 # https://pypi.org/project/black/ +isort==5.13.2 # https://pypi.org/project/isort/ +pur==7.3.2 # https://pypi.org/project/pur/ +pre-commit==4.0.1 # https://pypi.org/project/pre-commit/ +flake8==7.1.1 + +pytest==8.3.3 # https://pypi.org/project/pytest/ +coverage==7.6.8 # https://pypi.org/project/coverage/ + +mypy==1.13.0 # https://pypi.org/project/mypy/ +types-python-jose==3.3.4.20240106 # https://pypi.org/project/types-python-jose/ +types-passlib==1.7.7.20240819 # https://pypi.org/project/types-passlib/ +types-PyYAML==6.0.12.20240917 +types-Pygments==2.18.0.20240506 +types-colorama==0.4.15.20240311 +types-decorator==5.1.8.20240310 +types-six==1.16.21.20241105 +types-ujson==5.10.0.20240515 + +settings-doc==4.3.1 # https://github.com/radeklat/settings-doc +ipython==8.30.0 diff --git a/{{ cookiecutter.project_slug }}/requirements/production.txt b/{{ cookiecutter.project_slug }}/requirements/production.txt new file mode 100644 index 0000000..dc26733 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/requirements/production.txt @@ -0,0 +1,3 @@ +-r base.txt + +sentry-sdk[fastapi]==2.19.0 # https://pypi.org/project/sentry-sdk/ diff --git a/{{ cookiecutter.project_slug }}/service/__init__.py b/{{ cookiecutter.project_slug }}/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/api/__init__.py b/{{ cookiecutter.project_slug }}/service/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/api/dependencies.py b/{{ cookiecutter.project_slug }}/service/api/dependencies.py new file mode 100644 index 0000000..3f50bda --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/dependencies.py @@ -0,0 +1,72 @@ +from typing import Annotated + +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from pydantic import ValidationError +from setech.utils import get_logger +from starlette import status + +from service.api.models.auth import TokenPayload +from service.config import settings +from service.constants import security +from service.constants.types import PaginationParams +from service.database.models import AnonymousUser, User + +__all__ = ["LoggedInUser", "QueryParams", "CurrentRequestUser", "RequestUser"] +_l = get_logger("api") + +reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.root_path}/login/access-token", auto_error=False) + +TokenDep = Annotated[str | None, Depends(reusable_oauth2)] +RequestUser = User | AnonymousUser + + +async def get_current_user(token: TokenDep) -> User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + if token is None: + raise credentials_exception + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[security.ALGORITHM]) + token_data = TokenPayload(**payload) + except (JWTError, ValidationError, AttributeError): + raise credentials_exception + user = await User.filter(username=token_data.sub).first() + if user is None: + raise credentials_exception + return user + + +LoggedInUser = Annotated[User, Depends(get_current_user)] + + +async def get_request_user(token: TokenDep) -> RequestUser: + if not token: + return AnonymousUser() + try: + user = await get_current_user(token) + except HTTPException: + return AnonymousUser() + return user + + +CurrentRequestUser = Annotated[RequestUser, Depends(get_request_user)] + + +def query_params(q: str | None = None, page: int = 1, limit: int = 10, order: str | None = None) -> PaginationParams: + page -= 1 + if page < 0: + page = 0 + if limit < 0: + limit = 1 + if limit > 250: + limit = 250 + _l.info(f"Filtering by: {q=}, {page=}, {limit=} | Ordering by: {order}") + return PaginationParams(q=q, offset=page * limit, limit=limit, order=order) + + +QueryParams = Annotated[PaginationParams, Depends(query_params)] diff --git a/{{ cookiecutter.project_slug }}/service/api/main.py b/{{ cookiecutter.project_slug }}/service/api/main.py new file mode 100644 index 0000000..2278b29 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/main.py @@ -0,0 +1,69 @@ +import time +from typing import Any, Callable + +from fastapi import FastAPI, Request +from setech.utils import get_logger +from starlette import status +from starlette.middleware.cors import CORSMiddleware +from starlette.responses import RedirectResponse, Response +from tortoise.contrib.fastapi import register_tortoise + +from service.api.routes import api_router +from service.config import TORTOISE_ORM, settings +from service.utils.web import do_init + +do_init() +logger = get_logger("api") + +app = FastAPI( + debug=settings.debug, + root_path=settings.root_path, + title=settings.project_name, + version="0.1.0", + servers=[ + {"url": "http://localhost:8000/", "description": "Local"}, + {"url": "https://test.{{ cookiecutter.project_slug }}.com/", "description": "Staging environment"}, + {"url": "https://{{ cookiecutter.project_slug }}.com/", "description": "Production environment"}, + ], +) + + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next: Callable[[Any], Any]) -> Response: + start_time = time.time() + if settings.debug: + logger.info( + f"Received request for '{request.url}'", + extra={ + "headers": request.headers, + "content": await request.body(), + "cookies": request.cookies, + "url": request.url, + }, + ) + response: Response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # [str(origin).strip("/") for origin in settings.cors_origins], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.include_router(api_router) + + +@app.get("/", tags=["Root redirect"]) +def root_view() -> RedirectResponse: + return RedirectResponse("/docs", status_code=status.HTTP_308_PERMANENT_REDIRECT) + + +register_tortoise( + app, + config=TORTOISE_ORM, + add_exception_handlers=True, +) diff --git a/{{ cookiecutter.project_slug }}/service/api/models/__init__.py b/{{ cookiecutter.project_slug }}/service/api/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/api/models/auth.py b/{{ cookiecutter.project_slug }}/service/api/models/auth.py new file mode 100644 index 0000000..3988ee4 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/models/auth.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class Token(BaseModel): + access_token: str + token_type: str = "bearer" + + +class TokenPayload(BaseModel): + sub: str | None = None diff --git a/{{ cookiecutter.project_slug }}/service/api/models/generic.py b/{{ cookiecutter.project_slug }}/service/api/models/generic.py new file mode 100644 index 0000000..04242df --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/models/generic.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from pydantic import AfterValidator, BaseModel, EmailStr + +from service.core.exceptions import InvalidEmail +from service.utils.validation import validate_email + + +def email_validation(email: EmailStr) -> str: + try: + validated = validate_email(email) + except InvalidEmail as e: + raise AssertionError(str(e)) + return validated.normalized + + +ValidEmail = Annotated[EmailStr, AfterValidator(email_validation)] + + +class Message(BaseModel): + message: str diff --git a/{{ cookiecutter.project_slug }}/service/api/routes/__init__.py b/{{ cookiecutter.project_slug }}/service/api/routes/__init__.py new file mode 100644 index 0000000..bf3500d --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/routes/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .auth import auth_router +from .users import user_router + +api_router = APIRouter() +api_router.include_router(auth_router, prefix="/login", tags=["Authorisation"]) +api_router.include_router(user_router, prefix="/users", tags=["Users"]) diff --git a/{{ cookiecutter.project_slug }}/service/api/routes/auth.py b/{{ cookiecutter.project_slug }}/service/api/routes/auth.py new file mode 100644 index 0000000..9196e5e --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/routes/auth.py @@ -0,0 +1,36 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm +from starlette import status + +from service.api.dependencies import LoggedInUser +from service.api.models.auth import Token +from service.core import security +from service.crud.user import UserMe, authenticate + +auth_router = router = APIRouter() + + +@router.post("/access-token") +async def login_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token: + """ + OAuth2 compatible token login, get an access token for future requests + """ + user = await authenticate(username=form_data.username, password=form_data.password) + if not user: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Incorrect email and/or password") + return Token(access_token=security.create_access_token(user.username)) + + +@router.post("/test-token", response_model=UserMe) +async def test_token(current_user: LoggedInUser) -> UserMe: + """ + Test access token + """ + return UserMe( + id=current_user.id, + email=current_user.email, + is_superuser=current_user.is_superuser, + username=current_user.username, + ) diff --git a/{{ cookiecutter.project_slug }}/service/api/routes/users.py b/{{ cookiecutter.project_slug }}/service/api/routes/users.py new file mode 100644 index 0000000..a207b73 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/api/routes/users.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, HTTPException +from setech.utils import get_logger + +from service.api.dependencies import QueryParams +from service.core.exceptions import InvalidEmail +from service.crud.user import UserMe, UserPublic, UserRegister, get_user_by_username, get_users, register_user + +user_router = router = APIRouter() +logger = get_logger("api") + + +@router.get("/list") +async def list_users_view(_params: QueryParams) -> list[UserPublic] | None: + return await get_users(_params) + + +@router.post("/register") +async def register_user_view(data: UserRegister) -> UserMe | None: + try: + if await get_user_by_username(username=data.email): + raise HTTPException(400, "Given email is already registered!") + except InvalidEmail as e: + logger.warning(f"Attempting to register with invalid email: '{data.email}'") + raise HTTPException(400, str(e)) + logger.info(f"Creating user with email: '{data.email}'") + user = await register_user(data) + return UserMe(id=user.id, username=user.username, email=user.email, is_superuser=user.is_superuser) diff --git a/{{ cookiecutter.project_slug }}/service/config/__init__.py b/{{ cookiecutter.project_slug }}/service/config/__init__.py new file mode 100644 index 0000000..cd2bc2a --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/config/__init__.py @@ -0,0 +1,90 @@ +import os +import pathlib + +from service.config._settings import ProjectSettings + +__all__ = ["ROOT_DIR", "TORTOISE_ORM", "settings", "LOGGING"] +ROOT_DIR = pathlib.Path(__file__).parent.parent.parent +settings = ProjectSettings() +os.environ.update({"APP_NAME": settings.project_name}) + +TORTOISE_ORM = { + "connections": {"default": settings.database_url}, + "apps": { + "service": { + "models": ["service.database.models", "aerich.models"], + "default_connection": "default", + }, + }, + "use_tz": True, +} + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": {}, + "root": {"level": settings.log_level}, + "formatters": { + "structured": { + "()": "setech.logging.LogJSONFormatter", + }, + "simple": { + "format": "[%(asctime)s] %(levelname)-4s: %(message)s", + "datefmt": "%F %T", + }, + "precise": { + "format": "[%(asctime)s][%(levelname)-4s][%(filename)s:%(funcName)s:%(lineno)s: %(message)s", + "datefmt": "%F %T %z", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "simple", + }, + "console_json": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "structured", + }, + "console_precise": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "precise", + }, + "query_file": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": "logs/query.log", + "formatter": "structured", + }, + }, + "loggers": { + "tasks": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + "service": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + "api": { + "level": "INFO", + "handlers": ["console_json"], + "propagate": False, + }, + "crud.journal": { + "level": "DEBUG", + "handlers": ["console_json"], + "propagate": False, + }, + "tortoise.db_client": { + "level": "DEBUG", + "handlers": ["console_precise", "query_file"], + "propagate": False, + }, + }, +} diff --git a/{{ cookiecutter.project_slug }}/service/config/_settings.py b/{{ cookiecutter.project_slug }}/service/config/_settings.py new file mode 100644 index 0000000..7572e8f --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/config/_settings.py @@ -0,0 +1,34 @@ +from typing import Annotated + +from pydantic import AnyUrl, BeforeValidator, Field, HttpUrl, RedisDsn + +from service.constants import LogLevel +from service.constants.system import Environment +from service.helpers.settings import ProjectBaseSettings, parse_cors + + +class ProjectSettings(ProjectBaseSettings): + # Fast API Settings + root_path: str = Field("", examples=["/api/v2", ""]) + project_name: str = Field("{{ cookiecutter.project_slug }}") + debug: bool = Field(False) + + # Security Settings + secret_key: str = Field("CHANGE_ME--8^&gnoqen9+&9usjpjnsw*lhfqnl45p!^hdvf*s*i--INSECURE") + token_expiration_days: int = Field(1) + cors_origins: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = [] + + # Service values + environment: Environment = Field(Environment.local, examples=[_ for _ in Environment]) + log_level: LogLevel = Field(LogLevel.info, examples=[_ for _ in LogLevel]) + + # Background task config + redis_url: RedisDsn = Field(RedisDsn(url="redis://redis:6379")) + database_url: str = Field("psql://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}") + + # Various + timezone: str = Field( + "Europe/Riga", + examples=["UTC", "Europe/Riga", "Europe/London", "US/Pacific"], + ) + sentry_url: HttpUrl | None = Field(None) diff --git a/{{ cookiecutter.project_slug }}/service/constants/__init__.py b/{{ cookiecutter.project_slug }}/service/constants/__init__.py new file mode 100644 index 0000000..287e6f3 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/constants/__init__.py @@ -0,0 +1,5 @@ +from .system import LogLevel + +__all__ = [ + "LogLevel", +] diff --git a/{{ cookiecutter.project_slug }}/service/constants/models.py b/{{ cookiecutter.project_slug }}/service/constants/models.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/constants/security.py b/{{ cookiecutter.project_slug }}/service/constants/security.py new file mode 100644 index 0000000..8d4cb59 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/constants/security.py @@ -0,0 +1 @@ +ALGORITHM = "HS256" diff --git a/{{ cookiecutter.project_slug }}/service/constants/system.py b/{{ cookiecutter.project_slug }}/service/constants/system.py new file mode 100644 index 0000000..12472f0 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/constants/system.py @@ -0,0 +1,16 @@ +from enum import StrEnum + + +class LogLevel(StrEnum): + debug = "DEBUG" + info = "INFO" + warning = "WARNING" + error = "ERROR" + critical = "CRITICAL" + + +class Environment(StrEnum): + local = "local" + tests = "testing" + staging = "staging" + prod = "production" diff --git a/{{ cookiecutter.project_slug }}/service/constants/types.py b/{{ cookiecutter.project_slug }}/service/constants/types.py new file mode 100644 index 0000000..839f4d3 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/constants/types.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class PaginationParams: + q: str | None + offset: int + limit: int + order: str | None + + @property + def as_dict(self) -> dict[str, str | int | None]: + return dict(q=self.q, offset=self.offset, limit=self.limit, order=self.order) diff --git a/{{ cookiecutter.project_slug }}/service/core/__init__.py b/{{ cookiecutter.project_slug }}/service/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/core/backend_pre_start.py b/{{ cookiecutter.project_slug }}/service/core/backend_pre_start.py new file mode 100644 index 0000000..b7bba1b --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/core/backend_pre_start.py @@ -0,0 +1,54 @@ +import asyncio +import logging + +from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed +from tortoise import Tortoise + +from service.config import TORTOISE_ORM +from service.utils.logging import init_logging + +init_logging() +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("service") + +max_tries = 2 # 25 * 1 # 5 minutes +wait_seconds = 5 + + +@retry( + stop=stop_after_attempt(max_tries), + wait=wait_fixed(wait_seconds), + before=before_log(logger, logging.INFO), + after=after_log(logger, logging.WARN), +) +async def init() -> None: + try: + await Tortoise.init(TORTOISE_ORM) + except Exception as e: + logger.error(e) + raise e + + +@retry( + stop=stop_after_attempt(max_tries), + wait=wait_fixed(wait_seconds), + before=before_log(logger, logging.INFO), + after=after_log(logger, logging.WARN), +) +async def init_db() -> None: + try: + await Tortoise.init(TORTOISE_ORM) + except Exception as e: + logger.exception(e) + raise e + + +def main() -> None: + logger.info("Initializing service") + loop = asyncio.get_event_loop() + loop.run_until_complete(init_db()) + logger.info("Service finished initializing") + + +if __name__ == "__main__": + main() diff --git a/{{ cookiecutter.project_slug }}/service/core/exceptions.py b/{{ cookiecutter.project_slug }}/service/core/exceptions.py new file mode 100644 index 0000000..67782e3 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/core/exceptions.py @@ -0,0 +1,2 @@ +class InvalidEmail(Exception): + pass diff --git a/{{ cookiecutter.project_slug }}/service/core/security.py b/{{ cookiecutter.project_slug }}/service/core/security.py new file mode 100644 index 0000000..757fdac --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/core/security.py @@ -0,0 +1,25 @@ +from datetime import datetime, timedelta +from typing import Any + +from jose import jwt +from passlib.context import CryptContext + +from service.config import settings +from service.constants.security import ALGORITHM + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def create_access_token(subject: str | Any) -> str: + expire = datetime.utcnow() + timedelta(days=settings.token_expiration_days) + to_encode = {"exp": expire, "sub": str(subject)} + encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) diff --git a/{{ cookiecutter.project_slug }}/service/crud/__init__.py b/{{ cookiecutter.project_slug }}/service/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/crud/user/__init__.py b/{{ cookiecutter.project_slug }}/service/crud/user/__init__.py new file mode 100644 index 0000000..f8b63f7 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/crud/user/__init__.py @@ -0,0 +1,17 @@ +from .methods import authenticate, get_user_by_email, get_user_by_username, get_users, register_user +from .models import UpdatePassword, UserCreate, UserMe, UserPublic, UserRegister, UsersPublic, UserUpdate + +__all__ = [ + "authenticate", + "get_user_by_email", + "get_user_by_username", + "get_users", + "register_user", + "UsersPublic", + "UserMe", + "UserPublic", + "UpdatePassword", + "UserUpdate", + "UserCreate", + "UserRegister", +] diff --git a/{{ cookiecutter.project_slug }}/service/crud/user/methods.py b/{{ cookiecutter.project_slug }}/service/crud/user/methods.py new file mode 100644 index 0000000..54c6a74 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/crud/user/methods.py @@ -0,0 +1,39 @@ +from service.core.security import verify_password +from service.database.models import User +from ..utils import order_queryset + +from ...api.dependencies import QueryParams +from .models import UserPublic, UserRegister + + +async def authenticate(*, username: str, password: str) -> User | None: + db_user = await get_user_by_username(username=username) + if not db_user: + return None + return db_user if verify_password(password, db_user.password) else None + + +async def get_user_by_email(*, email: str) -> User | None: + return await User.filter(email=email).first() + + +async def get_user_by_username(*, username: str) -> User | None: + return await User.filter(username=username).first() + + +async def get_users(filters: QueryParams) -> list[UserPublic]: + queryset = User.all() + if filters.q: + queryset = queryset.filter(username__icontains=filters.q) + queryset = order_queryset(queryset, filters, "-modified_at") + return [ + UserPublic(id=u.id, username=u.username) for u in await queryset.offset(filters.offset).limit(filters.limit) + ] + + +async def register_user(data: UserRegister) -> User: + user_object = User(email=data.email, username=data.username) + user_object.set_password(data.password) + await user_object.save() + await user_object.refresh_from_db() + return user_object diff --git a/{{ cookiecutter.project_slug }}/service/crud/user/models.py b/{{ cookiecutter.project_slug }}/service/crud/user/models.py new file mode 100644 index 0000000..8a9993d --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/crud/user/models.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel + +from service.api.models.generic import ValidEmail + +__all__ = ["UsersPublic", "UserMe", "UserPublic", "UpdatePassword", "UserUpdate", "UserCreate", "UserRegister"] + + +class UserBase(BaseModel): + email: ValidEmail + username: str + is_superuser: bool = False + + +class UserCreate(UserBase): + password: str + + +class UserRegister(BaseModel): + email: ValidEmail + username: str + password: str + + +class UserUpdate(UserBase): + email: ValidEmail | None = None # type: ignore + username: str + password: str | None = None + + +class UpdatePassword(BaseModel): + current_password: str + new_password: str + new_password_repeat: str + + +class UserPublic(BaseModel): + id: int + username: str + + +class UserMe(UserBase): + id: int + username: str + + +class UsersPublic(BaseModel): + data: list[UserPublic] + count: int diff --git a/{{ cookiecutter.project_slug }}/service/crud/utils.py b/{{ cookiecutter.project_slug }}/service/crud/utils.py new file mode 100644 index 0000000..a9c57e8 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/crud/utils.py @@ -0,0 +1,8 @@ +from tortoise.queryset import QuerySet + +from service.constants.types import PaginationParams + + +def order_queryset(qs: QuerySet, filters: PaginationParams, default: str) -> QuerySet: + ordering = [f for f in filters.order.split(",") if f.split("-")[-1] in qs.fields] + return qs.order_by(*(ordering or (default, ))) diff --git a/{{ cookiecutter.project_slug }}/service/database/__init__.py b/{{ cookiecutter.project_slug }}/service/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/database/models.py b/{{ cookiecutter.project_slug }}/service/database/models.py new file mode 100644 index 0000000..32f1277 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/database/models.py @@ -0,0 +1,42 @@ +from datetime import datetime +from typing import Literal + +from tortoise import fields +from tortoise.models import Model +from tortoise.validators import MinLengthValidator + +from service.core.security import get_password_hash +from service.database.validators import EmailValidator + +__all__ = [ + "User", + "AnonymousUser", +] + + +class TimestampMixin(Model): + id: int = fields.BigIntField(pk=True) + created_at: datetime = fields.DatetimeField(null=True, auto_now_add=True) + modified_at: datetime = fields.DatetimeField(null=True, auto_now=True) + + class Meta: + abstract = True + + +class User(TimestampMixin, Model): + email = fields.CharField(max_length=255, validators=[MinLengthValidator(5), EmailValidator(False)], unique=True) + username = fields.CharField(max_length=32, unique=True) + password = fields.CharField(max_length=256) + is_superuser = fields.BooleanField(default=False, description="Is user a SuperUser?") + + # Helper for auto-complete and typing + # model_set: fields.ReverseRelation["DatabaseModel"] + + def set_password(self, new_password: str) -> None: + self.password = get_password_hash(new_password) + + +class AnonymousUser: + id: Literal[0] = 0 + email: Literal[""] = "" + username: Literal[""] = "" diff --git a/{{ cookiecutter.project_slug }}/service/database/validators.py b/{{ cookiecutter.project_slug }}/service/database/validators.py new file mode 100644 index 0000000..9c5cc44 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/database/validators.py @@ -0,0 +1,16 @@ +from tortoise.exceptions import ValidationError +from tortoise.validators import Validator + +from service.core.exceptions import InvalidEmail +from service.utils.validation import validate_email + + +class EmailValidator(Validator): + def __init__(self, use_dns: bool = False): + self.use_dns = use_dns + + def __call__(self, value: str) -> None: + try: + validate_email(value, self.use_dns) + except InvalidEmail as e: + raise ValidationError(str(e)) diff --git a/{{ cookiecutter.project_slug }}/service/helpers/__init__.py b/{{ cookiecutter.project_slug }}/service/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/helpers/settings.py b/{{ cookiecutter.project_slug }}/service/helpers/settings.py new file mode 100644 index 0000000..46506dc --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/helpers/settings.py @@ -0,0 +1,46 @@ +from pathlib import Path +from typing import Any, Tuple, Type + +import yaml +from pydantic_settings import BaseSettings, InitSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict +from pydantic_settings.sources import ConfigFileSourceMixin, EnvSettingsSource + + +def parse_cors(v: Any) -> list[str] | str: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, list | str): + return v + raise ValueError(v) + + +class ProjectBaseSettings(BaseSettings): + model_config = SettingsConfigDict(yaml_file="envs.yml", yaml_file_encoding="utf-8", env_prefix="service") + + @classmethod + def settings_customise_sources( + cls, + settings_cls: Type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: EnvSettingsSource, # type: ignore + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> Tuple[PydanticBaseSettingsSource, ...]: + return ( + init_settings, + PrefixedYamlConfigSettingsSource(env_settings.env_prefix, settings_cls), + env_settings, + file_secret_settings, + ) + + +class PrefixedYamlConfigSettingsSource(InitSettingsSource, ConfigFileSourceMixin): + def __init__(self, prefix: str, settings_cls: type[BaseSettings]): + self.yaml_file_path = "envs.yml" + self.yaml_file_encoding = "utf-8" + self.yaml_data = self._read_files(self.yaml_file_path)[prefix] + super().__init__(settings_cls, self.yaml_data) + + def _read_file(self, file_path: Path) -> dict[str, dict[str, Any]]: + with open(file_path, encoding=self.yaml_file_encoding) as yaml_file: + return yaml.safe_load(yaml_file) diff --git a/{{ cookiecutter.project_slug }}/service/utils/__init__.py b/{{ cookiecutter.project_slug }}/service/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/service/utils/logging.py b/{{ cookiecutter.project_slug }}/service/utils/logging.py new file mode 100644 index 0000000..24fcb8d --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/utils/logging.py @@ -0,0 +1,33 @@ +import logging.config +from pathlib import Path + +from setech.utils import get_logger + +from service.config import settings + + +def init_logging() -> None: + from service.config import LOGGING + + Path("logs").mkdir(parents=True, exist_ok=True) + logging.config.dictConfig(LOGGING) + + +def initialize_sentry() -> None: + if settings.sentry_url: + try: + import sentry_sdk # type: ignore + from sentry_sdk.integrations.fastapi import FastApiIntegration # type: ignore + from sentry_sdk.integrations.starlette import StarletteIntegration # type: ignore + + sentry_sdk.init( + dsn=str(settings.sentry_url), + enable_tracing=True, + environment=settings.environment, + integrations=[ + StarletteIntegration(transaction_style="endpoint"), + FastApiIntegration(transaction_style="endpoint"), + ], + ) + except (Exception, ModuleNotFoundError, ImportError) as e: # noqa + get_logger().exception("Unable to set up Sentry integration", exc_info=e) diff --git a/{{ cookiecutter.project_slug }}/service/utils/time.py b/{{ cookiecutter.project_slug }}/service/utils/time.py new file mode 100644 index 0000000..99211ee --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/utils/time.py @@ -0,0 +1,20 @@ +import datetime +import zoneinfo + + +def time_now() -> datetime.datetime: + return datetime.datetime.now(zoneinfo.ZoneInfo("UTC")) + + +def time_utc_now() -> datetime.datetime: + return _time_in_timezone() + + +def _time_in_timezone( + dt: datetime.datetime | None = None, zone_info: str | zoneinfo.ZoneInfo = "UTC" +) -> datetime.datetime: + if dt is None: + dt = datetime.datetime.now(zoneinfo.ZoneInfo("UTC")) + if isinstance(zone_info, str): + zone_info = zoneinfo.ZoneInfo(zone_info) + return dt.astimezone(zone_info) diff --git a/{{ cookiecutter.project_slug }}/service/utils/validation.py b/{{ cookiecutter.project_slug }}/service/utils/validation.py new file mode 100644 index 0000000..b46ae69 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/utils/validation.py @@ -0,0 +1,16 @@ +import email_validator +from email_validator import EmailNotValidError, ValidatedEmail + +from service.core.exceptions import InvalidEmail + + +def validate_email(value: str, use_dns: bool = None) -> ValidatedEmail: + try: + return email_validator.validate_email( + value, + # globally_deliverable=use_dns, + check_deliverability=use_dns, + allow_domain_literal=False, + ) + except EmailNotValidError as e: + raise InvalidEmail(str(e)) diff --git a/{{ cookiecutter.project_slug }}/service/utils/web.py b/{{ cookiecutter.project_slug }}/service/utils/web.py new file mode 100644 index 0000000..28f6042 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/service/utils/web.py @@ -0,0 +1,6 @@ +from service.utils.logging import init_logging, initialize_sentry + + +def do_init() -> None: + init_logging() + initialize_sentry()