cofy-api 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. cofy_api-0.1.0/PKG-INFO +163 -0
  2. cofy_api-0.1.0/README.md +147 -0
  3. cofy_api-0.1.0/pyproject.toml +76 -0
  4. cofy_api-0.1.0/setup.cfg +4 -0
  5. cofy_api-0.1.0/src/cofy/cofy_api.py +43 -0
  6. cofy_api-0.1.0/src/cofy/db/alembic/env.py +52 -0
  7. cofy_api-0.1.0/src/cofy/db/base.py +5 -0
  8. cofy_api-0.1.0/src/cofy/db/cofy_db.py +104 -0
  9. cofy_api-0.1.0/src/cofy/db/database_backed_source.py +13 -0
  10. cofy_api-0.1.0/src/cofy/db/timestamp_mixin.py +9 -0
  11. cofy_api-0.1.0/src/cofy/docs_router.py +37 -0
  12. cofy_api-0.1.0/src/cofy/token_auth.py +70 -0
  13. cofy_api-0.1.0/src/cofy/worker.py +187 -0
  14. cofy_api-0.1.0/src/cofy_api.egg-info/PKG-INFO +163 -0
  15. cofy_api-0.1.0/src/cofy_api.egg-info/SOURCES.txt +40 -0
  16. cofy_api-0.1.0/src/cofy_api.egg-info/dependency_links.txt +1 -0
  17. cofy_api-0.1.0/src/cofy_api.egg-info/requires.txt +7 -0
  18. cofy_api-0.1.0/src/cofy_api.egg-info/top_level.txt +4 -0
  19. cofy_api-0.1.0/src/demo/db/generate.py +38 -0
  20. cofy_api-0.1.0/src/demo/db/migrate.py +12 -0
  21. cofy_api-0.1.0/src/demo/db/reset.py +12 -0
  22. cofy_api-0.1.0/src/demo/db/seed.py +24 -0
  23. cofy_api-0.1.0/src/demo/main.py +49 -0
  24. cofy_api-0.1.0/src/demo/worker.py +39 -0
  25. cofy_api-0.1.0/src/modules/members/migrations/537edd2211fa_make_members_email_and_activation_code_.py +32 -0
  26. cofy_api-0.1.0/src/modules/members/migrations/members_core_0001_members_core_initial.py +41 -0
  27. cofy_api-0.1.0/src/modules/members/model.py +10 -0
  28. cofy_api-0.1.0/src/modules/members/models/db_member.py +13 -0
  29. cofy_api-0.1.0/src/modules/members/module.py +41 -0
  30. cofy_api-0.1.0/src/modules/members/source.py +22 -0
  31. cofy_api-0.1.0/src/modules/members/sources/db_source.py +43 -0
  32. cofy_api-0.1.0/src/modules/members/tasks/sync_from_csv.py +26 -0
  33. cofy_api-0.1.0/src/modules/tariff/formats/kiwatt.py +54 -0
  34. cofy_api-0.1.0/src/modules/tariff/module.py +69 -0
  35. cofy_api-0.1.0/src/modules/tariff/sources/entsoe_day_ahead.py +39 -0
  36. cofy_api-0.1.0/src/shared/module.py +57 -0
  37. cofy_api-0.1.0/src/shared/timeseries/format.py +37 -0
  38. cofy_api-0.1.0/src/shared/timeseries/formats/csv.py +51 -0
  39. cofy_api-0.1.0/src/shared/timeseries/formats/json.py +47 -0
  40. cofy_api-0.1.0/src/shared/timeseries/model.py +17 -0
  41. cofy_api-0.1.0/src/shared/timeseries/module.py +131 -0
  42. cofy_api-0.1.0/src/shared/timeseries/source.py +10 -0
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: cofy-api
3
+ Version: 0.1.0
4
+ Summary: An open-source modular framework for ingesting, standardising, storing, and computing energy-related data.
5
+ Author: Jorg
6
+ Author-email: EnergyID <info@energieid.be>
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: alembic>=1.18.4
10
+ Requires-Dist: entsoe-py>=0.7.10
11
+ Requires-Dist: fastapi[standard]>=0.128.0
12
+ Requires-Dist: narwhals>=2.15.0
13
+ Requires-Dist: pandas>=3.0.0
14
+ Requires-Dist: polars>=1.37.1
15
+ Requires-Dist: saq[redis,web]>=0.26.1
16
+
17
+ **Cofy Cloud** is an open-source modular framework for ingesting, standardising, storing, and computing energy-related data, designed to run anywhere from local deployments to cloud environments.
18
+
19
+ Right now this is very much a work in progress.
20
+ With the development of a first proof of concept. This is not ready for production use and the api is likely to change significantly in the near future.
21
+
22
+ ## Setup
23
+ ### Install
24
+ *TODO*
25
+ ### Configure
26
+ *TODO*
27
+ ### Development
28
+ We use [astral](https://docs.astral.sh/) python tooling for our development environment.
29
+ We use [poethepoet](https://poethepoet.natn.io) to define some essential tasks.
30
+ The demo run task is also available as vscode execution task, making it easy to run and debug the demo application from within vscode.
31
+
32
+ #### Install/update dependencies:
33
+ First install [uv](https://docs.astral.sh/uv/) if you don't have it yet.
34
+
35
+ Then install/update dependencies:
36
+ ```sh
37
+ uv sync
38
+ ```
39
+
40
+ Install [poethepoet](https://poethepoet.natn.io) and [pre-commit](https://pre-commit.com/)
41
+ ```sh
42
+ uv tool install poethepoet
43
+ uv tool install pre-commit
44
+ ```
45
+
46
+ Activate [pre-commit](https://pre-commit.com/) hooks that enforce code style on every commit:
47
+ ```sh
48
+ pre-commit install
49
+ ```
50
+
51
+ #### Configure environment variables:
52
+ Our demo application uses some API keys for external services. You can provide these `.env.local` file in the root of the repository, following the structure of `.env.example`.
53
+
54
+ #### Set up the database:
55
+ The demo application uses a SQLite database. A different database can be configured via the environment variables.
56
+ To create the database and seed it with example data:
57
+
58
+ ```sh
59
+ poe db-seed
60
+ ```
61
+
62
+ This will run all pending migrations and load the example CSV data into the database.
63
+
64
+ #### Run development demo application:
65
+
66
+ ```sh
67
+ poe demo
68
+ ```
69
+
70
+ #### Run background worker:
71
+ We use Cofy Worker to run background jobs that perform data ingestion and processing tasks asynchronously.
72
+
73
+ The background worker requires a Redis instance to run. You can configure the Redis URL via the environment variables.
74
+ The simplest way to run Redis locally is via Docker:
75
+
76
+ ```sh
77
+ docker run -p 6379:6379 redis
78
+ ```
79
+
80
+ Then start the worker:
81
+
82
+ ```sh
83
+ poe worker
84
+ ```
85
+
86
+ #### Code style checks:
87
+ ```sh
88
+ poe lint # Check code style
89
+ poe format # Format code
90
+ poe check # Run type checks
91
+ ```
92
+
93
+ #### Run tests:
94
+ ```sh
95
+ poe test
96
+ ```
97
+ #### Build & publish
98
+ TODO
99
+
100
+ ## Database & Migrations
101
+
102
+ Cofy uses [Alembic](https://alembic.sqlalchemy.org/) for database migrations. Each module that needs database storage owns its own migration branch, keeping schemas independent and composable.
103
+
104
+ ### Commands
105
+
106
+ | Command | Description |
107
+ |---|---|
108
+ | `poe db-seed` | Run all migrations and seed the database with example data |
109
+ | `poe db-migrate` | Run all pending migrations |
110
+ | `poe db-reset` | Drop all tables and re-run migrations (⚠️ destroys all data) |
111
+ | `poe db-generate` | Generate a new migration file for a specific module branch |
112
+
113
+ ### Generating a migration
114
+
115
+ When you change a module's SQLAlchemy model, generate a migration for that module's branch:
116
+
117
+ ```sh
118
+ poe db-generate "add phone number to member" --head members_core@head --rev-id members_core_0002
119
+ ```
120
+
121
+ | Argument | Required | Description |
122
+ |---|---|---|
123
+ | `message` (positional) | ✅ | Short description of the change |
124
+ | `--head` | ✅ | Branch to extend, e.g. `members_core@head` |
125
+ | `--rev-id` | ❌ | Custom revision ID (Alembic generates one if omitted) |
126
+ | `--no-autogenerate` | ❌ | Create an empty migration instead of diffing model changes |
127
+
128
+ The generated file will be placed in the module's own `migrations/versions/` directory automatically.
129
+
130
+ ### How branches work
131
+
132
+ Each module declares its own Alembic branch label in its initial migration. This allows multiple modules to coexist in the same database without interfering with each other.
133
+
134
+ For example, the members module uses branch `members_core`:
135
+
136
+ ```
137
+ src/modules/members/migrations/versions/
138
+ ├── members_core_0001_members_core_initial.py # branch_labels = ("members_core",)
139
+ └── members_core_0002_add_phone_number.py # extends members_core@head
140
+ ```
141
+
142
+ A separate module `foo` would have its own branch `foo_core` with revisions in its own directory:
143
+
144
+ ```
145
+ src/modules/foo/migrations/versions/
146
+ ├── foo_core_0001_initial.py # branch_labels = ("foo_core",)
147
+ └── foo_core_0002_add_index.py # extends foo_core@head
148
+ ```
149
+
150
+ When `poe db-migrate` runs, Alembic upgrades all branches to their latest head — both `members_core` and `foo_core` are applied independently.
151
+
152
+ ### Extending a module's schema
153
+
154
+ If you build a custom implementation that extends an existing module's schema, you can create a new branch that depends on a specific revision:
155
+
156
+ ```python
157
+ revision = "members_custom_0001"
158
+ down_revision = None
159
+ branch_labels = ("members_custom",)
160
+ depends_on = "members_core_0001" # ensures the base schema exists first
161
+ ```
162
+
163
+ This guarantees the base tables are created before your extension runs, while keeping both migration chains independent.
@@ -0,0 +1,147 @@
1
+ **Cofy Cloud** is an open-source modular framework for ingesting, standardising, storing, and computing energy-related data, designed to run anywhere from local deployments to cloud environments.
2
+
3
+ Right now this is very much a work in progress.
4
+ With the development of a first proof of concept. This is not ready for production use and the api is likely to change significantly in the near future.
5
+
6
+ ## Setup
7
+ ### Install
8
+ *TODO*
9
+ ### Configure
10
+ *TODO*
11
+ ### Development
12
+ We use [astral](https://docs.astral.sh/) python tooling for our development environment.
13
+ We use [poethepoet](https://poethepoet.natn.io) to define some essential tasks.
14
+ The demo run task is also available as vscode execution task, making it easy to run and debug the demo application from within vscode.
15
+
16
+ #### Install/update dependencies:
17
+ First install [uv](https://docs.astral.sh/uv/) if you don't have it yet.
18
+
19
+ Then install/update dependencies:
20
+ ```sh
21
+ uv sync
22
+ ```
23
+
24
+ Install [poethepoet](https://poethepoet.natn.io) and [pre-commit](https://pre-commit.com/)
25
+ ```sh
26
+ uv tool install poethepoet
27
+ uv tool install pre-commit
28
+ ```
29
+
30
+ Activate [pre-commit](https://pre-commit.com/) hooks that enforce code style on every commit:
31
+ ```sh
32
+ pre-commit install
33
+ ```
34
+
35
+ #### Configure environment variables:
36
+ Our demo application uses some API keys for external services. You can provide these `.env.local` file in the root of the repository, following the structure of `.env.example`.
37
+
38
+ #### Set up the database:
39
+ The demo application uses a SQLite database. A different database can be configured via the environment variables.
40
+ To create the database and seed it with example data:
41
+
42
+ ```sh
43
+ poe db-seed
44
+ ```
45
+
46
+ This will run all pending migrations and load the example CSV data into the database.
47
+
48
+ #### Run development demo application:
49
+
50
+ ```sh
51
+ poe demo
52
+ ```
53
+
54
+ #### Run background worker:
55
+ We use Cofy Worker to run background jobs that perform data ingestion and processing tasks asynchronously.
56
+
57
+ The background worker requires a Redis instance to run. You can configure the Redis URL via the environment variables.
58
+ The simplest way to run Redis locally is via Docker:
59
+
60
+ ```sh
61
+ docker run -p 6379:6379 redis
62
+ ```
63
+
64
+ Then start the worker:
65
+
66
+ ```sh
67
+ poe worker
68
+ ```
69
+
70
+ #### Code style checks:
71
+ ```sh
72
+ poe lint # Check code style
73
+ poe format # Format code
74
+ poe check # Run type checks
75
+ ```
76
+
77
+ #### Run tests:
78
+ ```sh
79
+ poe test
80
+ ```
81
+ #### Build & publish
82
+ TODO
83
+
84
+ ## Database & Migrations
85
+
86
+ Cofy uses [Alembic](https://alembic.sqlalchemy.org/) for database migrations. Each module that needs database storage owns its own migration branch, keeping schemas independent and composable.
87
+
88
+ ### Commands
89
+
90
+ | Command | Description |
91
+ |---|---|
92
+ | `poe db-seed` | Run all migrations and seed the database with example data |
93
+ | `poe db-migrate` | Run all pending migrations |
94
+ | `poe db-reset` | Drop all tables and re-run migrations (⚠️ destroys all data) |
95
+ | `poe db-generate` | Generate a new migration file for a specific module branch |
96
+
97
+ ### Generating a migration
98
+
99
+ When you change a module's SQLAlchemy model, generate a migration for that module's branch:
100
+
101
+ ```sh
102
+ poe db-generate "add phone number to member" --head members_core@head --rev-id members_core_0002
103
+ ```
104
+
105
+ | Argument | Required | Description |
106
+ |---|---|---|
107
+ | `message` (positional) | ✅ | Short description of the change |
108
+ | `--head` | ✅ | Branch to extend, e.g. `members_core@head` |
109
+ | `--rev-id` | ❌ | Custom revision ID (Alembic generates one if omitted) |
110
+ | `--no-autogenerate` | ❌ | Create an empty migration instead of diffing model changes |
111
+
112
+ The generated file will be placed in the module's own `migrations/versions/` directory automatically.
113
+
114
+ ### How branches work
115
+
116
+ Each module declares its own Alembic branch label in its initial migration. This allows multiple modules to coexist in the same database without interfering with each other.
117
+
118
+ For example, the members module uses branch `members_core`:
119
+
120
+ ```
121
+ src/modules/members/migrations/versions/
122
+ ├── members_core_0001_members_core_initial.py # branch_labels = ("members_core",)
123
+ └── members_core_0002_add_phone_number.py # extends members_core@head
124
+ ```
125
+
126
+ A separate module `foo` would have its own branch `foo_core` with revisions in its own directory:
127
+
128
+ ```
129
+ src/modules/foo/migrations/versions/
130
+ ├── foo_core_0001_initial.py # branch_labels = ("foo_core",)
131
+ └── foo_core_0002_add_index.py # extends foo_core@head
132
+ ```
133
+
134
+ When `poe db-migrate` runs, Alembic upgrades all branches to their latest head — both `members_core` and `foo_core` are applied independently.
135
+
136
+ ### Extending a module's schema
137
+
138
+ If you build a custom implementation that extends an existing module's schema, you can create a new branch that depends on a specific revision:
139
+
140
+ ```python
141
+ revision = "members_custom_0001"
142
+ down_revision = None
143
+ branch_labels = ("members_custom",)
144
+ depends_on = "members_core_0001" # ensures the base schema exists first
145
+ ```
146
+
147
+ This guarantees the base tables are created before your extension runs, while keeping both migration chains independent.
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "cofy-api"
3
+ authors = [
4
+ { name="EnergyID", email = "info@energieid.be" },
5
+ { name="Jorg" }
6
+ ]
7
+ version = "0.1.0"
8
+ description = "An open-source modular framework for ingesting, standardising, storing, and computing energy-related data."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "alembic>=1.18.4",
13
+ "entsoe-py>=0.7.10",
14
+ "fastapi[standard]>=0.128.0",
15
+ "narwhals>=2.15.0",
16
+ "pandas>=3.0.0",
17
+ "polars>=1.37.1",
18
+ "saq[redis,web]>=0.26.1",
19
+ ]
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest-asyncio>=1.3.0",
23
+ "pytest-cov>=7.0.0",
24
+ "pytest>=9.0.2",
25
+ "ruff>=0.14.13",
26
+ "ty>=0.0.12",
27
+ ]
28
+ [tool.poe.tasks]
29
+ lint = "ruff check --fix"
30
+ format = "ruff format"
31
+ check = "ty check"
32
+ [tool.poe.tasks.demo]
33
+ cmd = "fastapi dev src/demo/main.py"
34
+ env = { PYTHONPATH = "." }
35
+ envfile = ".env.local"
36
+ [tool.poe.tasks.db-seed]
37
+ cmd = "python -m src.demo.db.seed"
38
+ envfile = ".env.local"
39
+ env = { PYTHONPATH = "." }
40
+ [tool.poe.tasks.db-reset]
41
+ cmd = "python -m src.demo.db.reset"
42
+ envfile = ".env.local"
43
+ [tool.poe.tasks.db-migrate]
44
+ cmd = "python -m src.demo.db.migrate"
45
+ envfile = ".env.local"
46
+ env = { PYTHONPATH = "." }
47
+ [tool.poe.tasks.db-generate]
48
+ cmd = "python -m src.demo.db.generate"
49
+ envfile = ".env.local"
50
+ env = { PYTHONPATH = "." }
51
+ [tool.poe.tasks.worker]
52
+ cmd = "saq src.demo.worker.settings"
53
+ env = { PYTHONPATH = "." }
54
+ envfile = ".env.local"
55
+ [tool.poe.tasks.test]
56
+ cmd = "pytest --cov=src --cov-report=term-missing --import-mode=importlib"
57
+ env = { PYTHONPATH = "." }
58
+ [tool.coverage.report]
59
+ skip_covered = true
60
+ [tool.coverage.run]
61
+ source = ["src"]
62
+ omit = ["src/demo/*", "src/modules/*/migrations/*", "src/cofy/db/alembic/*"]
63
+ [tool.ruff]
64
+ line-length = 120
65
+ [tool.ruff.lint]
66
+ select = [
67
+ "E", # pycodestyle
68
+ "F", # Pyflakes
69
+ "UP", # pyupgrade
70
+ "B", # flake8-bugbear
71
+ "SIM", # flake8-simplify
72
+ "I", # isort
73
+ ]
74
+ ignore = [
75
+ "E501", #Line too long
76
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,43 @@
1
+ from typing import Any
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.openapi.utils import get_openapi
5
+
6
+ from src.cofy.docs_router import DocsRouter
7
+ from src.shared.module import Module
8
+
9
+ DEFAULT_ARGS: dict[str, Any] = {
10
+ "title": "Cofy API",
11
+ "version": "0.1.0",
12
+ "description": "Modular cloud API for energy data",
13
+ "docs_url": None,
14
+ "redoc_url": None,
15
+ "openapi_url": None,
16
+ }
17
+
18
+
19
+ class CofyApi(FastAPI):
20
+ def __init__(self, **kwargs):
21
+ super().__init__(**(DEFAULT_ARGS | kwargs))
22
+ self._modules: list[Module] = []
23
+ self.include_router(DocsRouter(self.openapi))
24
+
25
+ def openapi(self):
26
+ return get_openapi(
27
+ title=self.title,
28
+ version=self.version,
29
+ routes=self.routes,
30
+ tags=self.tags_metadata,
31
+ )
32
+
33
+ def register_module(self, module: Module):
34
+ self._modules.append(module)
35
+ self.include_router(module)
36
+
37
+ @property
38
+ def tags_metadata(self) -> list[dict[str, Any]]:
39
+ return [module.tag for module in self._modules]
40
+
41
+ @property
42
+ def modules(self) -> tuple[Module, ...]:
43
+ return tuple(self._modules)
@@ -0,0 +1,52 @@
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from sqlalchemy import engine_from_config, pool
5
+
6
+ from src.cofy.db.base import Base
7
+
8
+ config = context.config
9
+
10
+ if config.config_file_name is not None:
11
+ fileConfig(config.config_file_name)
12
+
13
+ configured_metadata = config.attributes.get("target_metadata")
14
+ if isinstance(configured_metadata, list):
15
+ target_metadata = configured_metadata[0] if configured_metadata else Base.metadata
16
+ elif configured_metadata is not None:
17
+ target_metadata = configured_metadata
18
+ else:
19
+ target_metadata = Base.metadata
20
+
21
+
22
+ def run_migrations_offline() -> None:
23
+ url = config.get_main_option("sqlalchemy.url")
24
+ context.configure(
25
+ url=url,
26
+ target_metadata=target_metadata,
27
+ literal_binds=True,
28
+ dialect_opts={"paramstyle": "named"},
29
+ )
30
+
31
+ with context.begin_transaction():
32
+ context.run_migrations()
33
+
34
+
35
+ def run_migrations_online() -> None:
36
+ connectable = engine_from_config(
37
+ config.get_section(config.config_ini_section, {}),
38
+ prefix="sqlalchemy.",
39
+ poolclass=pool.NullPool,
40
+ )
41
+
42
+ with connectable.connect() as connection:
43
+ context.configure(connection=connection, target_metadata=target_metadata)
44
+
45
+ with context.begin_transaction():
46
+ context.run_migrations()
47
+
48
+
49
+ if context.is_offline_mode():
50
+ run_migrations_offline()
51
+ else:
52
+ run_migrations_online()
@@ -0,0 +1,5 @@
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
3
+
4
+ class Base(DeclarativeBase):
5
+ pass
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from importlib import resources
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from alembic import command
9
+ from alembic.config import Config
10
+ from sqlalchemy import MetaData, create_engine
11
+
12
+ from src.cofy.db.database_backed_source import DatabaseBackedSource
13
+
14
+ if TYPE_CHECKING:
15
+ from sqlalchemy.engine import Engine
16
+
17
+ from src.cofy.cofy_api import CofyApi
18
+ from src.shared.module import Module
19
+
20
+
21
+ class CofyDB:
22
+ def __init__(self, url: str | None = None, **engine_kwargs):
23
+ if url is None:
24
+ raise ValueError("CofyDB requires a database URL to be configured.")
25
+ self._sources: list[DatabaseBackedSource] = []
26
+ self._url = url
27
+ self.engine: Engine = create_engine(self._url, **engine_kwargs)
28
+
29
+ def register_module(self, module: Module):
30
+ source = getattr(module, "source", None)
31
+ if isinstance(source, DatabaseBackedSource):
32
+ self._sources.append(source)
33
+
34
+ def register_modules(self, modules: Iterable[Module]):
35
+ for module in modules:
36
+ self.register_module(module)
37
+
38
+ def bind_api(self, api: CofyApi):
39
+ self.register_modules(api.modules)
40
+
41
+ @property
42
+ def migration_locations(self) -> list[str]:
43
+ locations: list[str] = []
44
+ for source in self._sources:
45
+ for location in source.migration_locations:
46
+ resolved_location = str(Path(location).resolve())
47
+ if resolved_location not in locations:
48
+ locations.append(resolved_location)
49
+ return locations
50
+
51
+ @property
52
+ def target_metadata(self) -> list[Any]:
53
+ metadata: list[Any] = []
54
+ for source in self._sources:
55
+ if source.target_metadata is not None and source.target_metadata not in metadata:
56
+ metadata.append(source.target_metadata)
57
+ return metadata
58
+
59
+ def _build_config(self) -> Config:
60
+ config = Config()
61
+ config.set_main_option("script_location", str(resources.files("src.cofy.db").joinpath("alembic")))
62
+ config.set_main_option("sqlalchemy.url", self._url)
63
+ config.set_main_option("path_separator", "os")
64
+
65
+ if self.migration_locations:
66
+ config.set_main_option("version_locations", " ".join(self.migration_locations))
67
+
68
+ config.attributes["target_metadata"] = self.target_metadata
69
+ return config
70
+
71
+ def run_migrations(self, revision: str = "heads") -> None:
72
+ command.upgrade(self._build_config(), revision)
73
+
74
+ def generate_migration(
75
+ self,
76
+ message: str,
77
+ head: str,
78
+ rev_id: str | None = None,
79
+ autogenerate: bool = True,
80
+ ) -> None:
81
+ """Generate a new Alembic migration revision for a specific module branch.
82
+
83
+ Args:
84
+ message: Description of the migration.
85
+ head: The branch head to extend, e.g. "members_core@head".
86
+ rev_id: Optional custom revision ID. If None, Alembic generates one.
87
+ autogenerate: Whether to autogenerate the migration from model changes.
88
+ """
89
+ command.revision(
90
+ self._build_config(),
91
+ message=message,
92
+ autogenerate=autogenerate,
93
+ head=head,
94
+ rev_id=rev_id,
95
+ )
96
+
97
+ def reset(self) -> None:
98
+ """usefull in development to reset the database to a clean state
99
+ Warning: this will delete all data in the database, use with caution!
100
+ """
101
+ meta = MetaData()
102
+ meta.reflect(bind=self.engine)
103
+ meta.drop_all(bind=self.engine)
104
+ self.run_migrations()
@@ -0,0 +1,13 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any, Protocol, runtime_checkable
3
+
4
+
5
+ @runtime_checkable
6
+ class DatabaseBackedSource(Protocol):
7
+ @property
8
+ def migration_locations(self) -> Sequence[str]:
9
+ """Filesystem paths containing alembic version files."""
10
+
11
+ @property
12
+ def target_metadata(self) -> Any | None:
13
+ """SQLAlchemy metadata used for migration autogeneration."""
@@ -0,0 +1,9 @@
1
+ import datetime as dt
2
+
3
+ from sqlalchemy import DateTime, func
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+
6
+
7
+ class TimestampMixin:
8
+ created_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=func.now())
9
+ updated_at: Mapped[dt.datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
@@ -0,0 +1,37 @@
1
+ from collections.abc import Callable
2
+
3
+ from fastapi import APIRouter, Request
4
+ from fastapi.openapi.docs import get_swagger_ui_html
5
+ from fastapi.responses import HTMLResponse
6
+
7
+
8
+ class DocsRouter(APIRouter):
9
+ def __init__(self, get_openapi: Callable[[], dict]):
10
+ super().__init__()
11
+ self.get_openapi = get_openapi
12
+ self.add_api_route(
13
+ "/docs",
14
+ self.get_swagger_ui_html,
15
+ include_in_schema=False,
16
+ )
17
+ self.add_api_route("/openapi.json", self.get_openapi, include_in_schema=False)
18
+
19
+ async def get_swagger_ui_html(self, request: Request):
20
+ response = get_swagger_ui_html(
21
+ openapi_url="/openapi.json",
22
+ title="Docs",
23
+ swagger_ui_parameters={
24
+ "spec": self.get_openapi(),
25
+ "onComplete": "AUTHORIZE_API",
26
+ },
27
+ )
28
+
29
+ if hasattr(request.state, "auth_info"):
30
+ assert isinstance(response.body, bytes)
31
+ content = response.body.decode()
32
+ content = content.replace(
33
+ '"AUTHORIZE_API"',
34
+ f'() => ui.preauthorizeApiKey("{request.state.auth_info["scheme"]}", "{request.state.auth_info["content"]}")',
35
+ )
36
+ response = HTMLResponse(content=content, status_code=response.status_code)
37
+ return response