servicekit 1.0.1__tar.gz → 2.0.1__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.
- {servicekit-1.0.1 → servicekit-2.0.1}/PKG-INFO +35 -3
- {servicekit-1.0.1 → servicekit-2.0.1}/README.md +34 -2
- servicekit-2.0.1/pyproject.toml +180 -0
- servicekit-1.0.1/pyproject.toml → servicekit-2.0.1/pyproject.toml.orig +4 -1
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/__init__.py +4 -1
- servicekit-2.0.1/src/servicekit/alembic/env.py +76 -0
- servicekit-2.0.1/src/servicekit/alembic/script.py.mako +26 -0
- servicekit-2.0.1/src/servicekit/alembic/versions/20251010_0927_4d869b5fb06e_initial_schema.py +27 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/__init__.py +5 -14
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/app.py +19 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/crud.py +39 -18
- servicekit-2.0.1/src/servicekit/api/dependencies.py +55 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/middleware.py +71 -11
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/monitoring.py +17 -6
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/registration.py +51 -54
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/routers/health.py +10 -3
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/routers/job.py +29 -17
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/service_builder.py +218 -111
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/database.py +31 -5
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/exceptions.py +16 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/manager.py +43 -9
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/repository.py +26 -6
- servicekit-2.0.1/src/servicekit/scheduler.py +500 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/schemas.py +7 -5
- servicekit-1.0.1/src/servicekit/api/dependencies.py +0 -69
- servicekit-1.0.1/src/servicekit/scheduler.py +0 -302
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/apps/landing/index.html +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/apps/landing/manifest.json +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/auth.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/pagination.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/router.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/routers/__init__.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/routers/metrics.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/routers/system.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/sse.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/api/utilities.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/gunicorn.conf.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/logging.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/models.py +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/py.typed +0 -0
- {servicekit-1.0.1 → servicekit-2.0.1}/src/servicekit/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: servicekit
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services
|
|
5
5
|
Keywords: fastapi,sqlalchemy,async,database,rest-api,crud,framework
|
|
6
6
|
Author: Morten Hansen
|
|
@@ -89,7 +89,7 @@ servicekit/
|
|
|
89
89
|
├── repository.py # Repository, BaseRepository
|
|
90
90
|
├── manager.py # Manager, BaseManager
|
|
91
91
|
├── schemas.py # EntityIn, EntityOut, PaginatedResponse
|
|
92
|
-
├── scheduler.py #
|
|
92
|
+
├── scheduler.py # Scheduler, InMemoryScheduler
|
|
93
93
|
├── exceptions.py # Error classes
|
|
94
94
|
├── logging.py # Structured logging
|
|
95
95
|
├── types.py # ULIDType, JsonSafe
|
|
@@ -122,6 +122,32 @@ app = (
|
|
|
122
122
|
)
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### Database Migrations
|
|
126
|
+
|
|
127
|
+
File-based databases run Alembic migrations on `init()` by default; in-memory databases create tables
|
|
128
|
+
directly from the ORM metadata. The migration bundled with servicekit is a framework baseline: it creates
|
|
129
|
+
only the `alembic_version` table and no application tables. Applications that define their own entities must
|
|
130
|
+
either point migrations at their own migration directory or disable migrations and create tables directly.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pathlib import Path
|
|
134
|
+
|
|
135
|
+
from servicekit import SqliteDatabaseBuilder, get_alembic_dir
|
|
136
|
+
|
|
137
|
+
# Application-owned migrations
|
|
138
|
+
database = (
|
|
139
|
+
SqliteDatabaseBuilder.from_file("app.db")
|
|
140
|
+
.with_migrations(enabled=True, alembic_dir=Path("alembic"))
|
|
141
|
+
.build()
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# No migrations - create tables directly from ORM metadata
|
|
145
|
+
database = SqliteDatabaseBuilder.from_file("app.db").with_migrations(enabled=False).build()
|
|
146
|
+
|
|
147
|
+
# Path to the migration environment shipped inside the servicekit package
|
|
148
|
+
bundled_migrations = get_alembic_dir()
|
|
149
|
+
```
|
|
150
|
+
|
|
125
151
|
### Repository Pattern
|
|
126
152
|
|
|
127
153
|
```python
|
|
@@ -153,6 +179,11 @@ router = CrudRouter.create(
|
|
|
153
179
|
)
|
|
154
180
|
```
|
|
155
181
|
|
|
182
|
+
`POST` creates only and returns `409 Conflict` for an ID that already exists or for any other database constraint
|
|
183
|
+
violation. `PUT` applies the fields sent: an omitted field keeps its value, an explicit `null` clears a nullable
|
|
184
|
+
field. Collection listings are ordered by ID, and pagination is opt-in through `page` (>= 1) and `size` (1-100),
|
|
185
|
+
which return `422` when out of range.
|
|
186
|
+
|
|
156
187
|
|
|
157
188
|
## Examples
|
|
158
189
|
|
|
@@ -172,7 +203,8 @@ See `docs/` for comprehensive guides and API reference.
|
|
|
172
203
|
|
|
173
204
|
```bash
|
|
174
205
|
make test # Run tests
|
|
175
|
-
make
|
|
206
|
+
make format # Format code and apply lint fixes
|
|
207
|
+
make lint # Check formatting, linting and types
|
|
176
208
|
make coverage # Test coverage
|
|
177
209
|
```
|
|
178
210
|
|
|
@@ -53,7 +53,7 @@ servicekit/
|
|
|
53
53
|
├── repository.py # Repository, BaseRepository
|
|
54
54
|
├── manager.py # Manager, BaseManager
|
|
55
55
|
├── schemas.py # EntityIn, EntityOut, PaginatedResponse
|
|
56
|
-
├── scheduler.py #
|
|
56
|
+
├── scheduler.py # Scheduler, InMemoryScheduler
|
|
57
57
|
├── exceptions.py # Error classes
|
|
58
58
|
├── logging.py # Structured logging
|
|
59
59
|
├── types.py # ULIDType, JsonSafe
|
|
@@ -86,6 +86,32 @@ app = (
|
|
|
86
86
|
)
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Database Migrations
|
|
90
|
+
|
|
91
|
+
File-based databases run Alembic migrations on `init()` by default; in-memory databases create tables
|
|
92
|
+
directly from the ORM metadata. The migration bundled with servicekit is a framework baseline: it creates
|
|
93
|
+
only the `alembic_version` table and no application tables. Applications that define their own entities must
|
|
94
|
+
either point migrations at their own migration directory or disable migrations and create tables directly.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from pathlib import Path
|
|
98
|
+
|
|
99
|
+
from servicekit import SqliteDatabaseBuilder, get_alembic_dir
|
|
100
|
+
|
|
101
|
+
# Application-owned migrations
|
|
102
|
+
database = (
|
|
103
|
+
SqliteDatabaseBuilder.from_file("app.db")
|
|
104
|
+
.with_migrations(enabled=True, alembic_dir=Path("alembic"))
|
|
105
|
+
.build()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# No migrations - create tables directly from ORM metadata
|
|
109
|
+
database = SqliteDatabaseBuilder.from_file("app.db").with_migrations(enabled=False).build()
|
|
110
|
+
|
|
111
|
+
# Path to the migration environment shipped inside the servicekit package
|
|
112
|
+
bundled_migrations = get_alembic_dir()
|
|
113
|
+
```
|
|
114
|
+
|
|
89
115
|
### Repository Pattern
|
|
90
116
|
|
|
91
117
|
```python
|
|
@@ -117,6 +143,11 @@ router = CrudRouter.create(
|
|
|
117
143
|
)
|
|
118
144
|
```
|
|
119
145
|
|
|
146
|
+
`POST` creates only and returns `409 Conflict` for an ID that already exists or for any other database constraint
|
|
147
|
+
violation. `PUT` applies the fields sent: an omitted field keeps its value, an explicit `null` clears a nullable
|
|
148
|
+
field. Collection listings are ordered by ID, and pagination is opt-in through `page` (>= 1) and `size` (1-100),
|
|
149
|
+
which return `422` when out of range.
|
|
150
|
+
|
|
120
151
|
|
|
121
152
|
## Examples
|
|
122
153
|
|
|
@@ -136,7 +167,8 @@ See `docs/` for comprehensive guides and API reference.
|
|
|
136
167
|
|
|
137
168
|
```bash
|
|
138
169
|
make test # Run tests
|
|
139
|
-
make
|
|
170
|
+
make format # Format code and apply lint fixes
|
|
171
|
+
make lint # Check formatting, linting and types
|
|
140
172
|
make coverage # Test coverage
|
|
141
173
|
```
|
|
142
174
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "servicekit"
|
|
3
|
+
version = "2.0.1"
|
|
4
|
+
description = "Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13,<3.14"
|
|
7
|
+
keywords = [
|
|
8
|
+
"fastapi",
|
|
9
|
+
"sqlalchemy",
|
|
10
|
+
"async",
|
|
11
|
+
"database",
|
|
12
|
+
"rest-api",
|
|
13
|
+
"crud",
|
|
14
|
+
"framework",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Framework :: FastAPI",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Database",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"aiosqlite>=0.21.0",
|
|
27
|
+
"alembic>=1.17.0",
|
|
28
|
+
"fastapi[standard]>=0.136.3",
|
|
29
|
+
"gunicorn>=23.0.0",
|
|
30
|
+
"opentelemetry-api>=1.37.0",
|
|
31
|
+
"opentelemetry-exporter-prometheus>=0.58b0",
|
|
32
|
+
"opentelemetry-instrumentation-fastapi>=0.58b0",
|
|
33
|
+
"opentelemetry-instrumentation-sqlalchemy>=0.58b0",
|
|
34
|
+
"opentelemetry-sdk>=1.37.0",
|
|
35
|
+
"pydantic>=2.12.0",
|
|
36
|
+
"python-ulid>=3.1.0",
|
|
37
|
+
"sqlalchemy[asyncio]>=2.0.43",
|
|
38
|
+
"starlette>=1.0.1",
|
|
39
|
+
"structlog>=24.4.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[project.authors]]
|
|
43
|
+
name = "Morten Hansen"
|
|
44
|
+
email = "morten@dhis2.org"
|
|
45
|
+
|
|
46
|
+
[project.license]
|
|
47
|
+
text = "AGPL-3.0-or-later"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/dhis2-chap/servicekit"
|
|
53
|
+
Repository = "https://github.com/dhis2-chap/servicekit"
|
|
54
|
+
Issues = "https://github.com/dhis2-chap/servicekit/issues"
|
|
55
|
+
Documentation = "https://dhis2-chap.github.io/servicekit"
|
|
56
|
+
|
|
57
|
+
[dependency-groups]
|
|
58
|
+
dev = [
|
|
59
|
+
"coverage[toml]>=7.13.2",
|
|
60
|
+
"mypy>=1.19.1",
|
|
61
|
+
"pytest>=9.0.2",
|
|
62
|
+
"pytest-cov>=5.0.0",
|
|
63
|
+
"pytest-asyncio>=1.2.0",
|
|
64
|
+
"ruff>=0.14.14",
|
|
65
|
+
"pyright>=1.1.408",
|
|
66
|
+
"mkdocs>=1.6.0",
|
|
67
|
+
"mkdocs-material>=9.7.1",
|
|
68
|
+
"mkdocstrings[python]>=1.0.2",
|
|
69
|
+
"httpx2>=2.4.0",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[build-system]
|
|
73
|
+
requires = ["uv_build>=0.9.0,<0.12.0"]
|
|
74
|
+
build-backend = "uv_build"
|
|
75
|
+
|
|
76
|
+
[tool.ruff]
|
|
77
|
+
target-version = "py313"
|
|
78
|
+
line-length = 120
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
fixable = ["ALL"]
|
|
82
|
+
select = [
|
|
83
|
+
"E",
|
|
84
|
+
"W",
|
|
85
|
+
"F",
|
|
86
|
+
"I",
|
|
87
|
+
"D",
|
|
88
|
+
]
|
|
89
|
+
ignore = [
|
|
90
|
+
"D203",
|
|
91
|
+
"D213",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.isort]
|
|
95
|
+
known-third-party = ["alembic"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.pydocstyle]
|
|
98
|
+
convention = "google"
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.per-file-ignores]
|
|
101
|
+
"tests/**/*.py" = ["D"]
|
|
102
|
+
"**/__init__.py" = ["D104"]
|
|
103
|
+
"src/**/*.py" = [
|
|
104
|
+
"D102",
|
|
105
|
+
"D105",
|
|
106
|
+
"D107",
|
|
107
|
+
]
|
|
108
|
+
"examples/**/*.py" = []
|
|
109
|
+
|
|
110
|
+
[tool.ruff.format]
|
|
111
|
+
quote-style = "double"
|
|
112
|
+
indent-style = "space"
|
|
113
|
+
skip-magic-trailing-comma = false
|
|
114
|
+
docstring-code-format = true
|
|
115
|
+
docstring-code-line-length = "dynamic"
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
asyncio_mode = "auto"
|
|
119
|
+
testpaths = ["tests"]
|
|
120
|
+
norecursedirs = [
|
|
121
|
+
"examples",
|
|
122
|
+
".git",
|
|
123
|
+
".venv",
|
|
124
|
+
"__pycache__",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[tool.coverage.run]
|
|
128
|
+
branch = true
|
|
129
|
+
dynamic_context = "test_function"
|
|
130
|
+
relative_files = true
|
|
131
|
+
source = ["servicekit"]
|
|
132
|
+
|
|
133
|
+
[tool.coverage.report]
|
|
134
|
+
exclude_also = ["if TYPE_CHECKING:"]
|
|
135
|
+
precision = 2
|
|
136
|
+
show_missing = true
|
|
137
|
+
skip_covered = true
|
|
138
|
+
|
|
139
|
+
[tool.mypy]
|
|
140
|
+
python_version = "3.13"
|
|
141
|
+
warn_return_any = true
|
|
142
|
+
warn_unused_configs = true
|
|
143
|
+
disallow_untyped_defs = true
|
|
144
|
+
check_untyped_defs = true
|
|
145
|
+
disallow_any_unimported = true
|
|
146
|
+
no_implicit_optional = true
|
|
147
|
+
warn_unused_ignores = true
|
|
148
|
+
strict_equality = true
|
|
149
|
+
mypy_path = ["src"]
|
|
150
|
+
|
|
151
|
+
[[tool.mypy.overrides]]
|
|
152
|
+
module = "tests.*"
|
|
153
|
+
disallow_untyped_defs = false
|
|
154
|
+
|
|
155
|
+
[[tool.mypy.overrides]]
|
|
156
|
+
module = "examples.*"
|
|
157
|
+
ignore_missing_imports = true
|
|
158
|
+
disallow_untyped_defs = false
|
|
159
|
+
ignore_errors = true
|
|
160
|
+
|
|
161
|
+
[tool.pyright]
|
|
162
|
+
include = [
|
|
163
|
+
"src",
|
|
164
|
+
"tests",
|
|
165
|
+
"examples",
|
|
166
|
+
]
|
|
167
|
+
pythonVersion = "3.13"
|
|
168
|
+
typeCheckingMode = "strict"
|
|
169
|
+
useLibraryCodeForTypes = true
|
|
170
|
+
reportPrivateUsage = false
|
|
171
|
+
reportUnusedFunction = false
|
|
172
|
+
reportUnknownMemberType = false
|
|
173
|
+
reportUnknownArgumentType = false
|
|
174
|
+
reportUnknownLambdaType = false
|
|
175
|
+
reportUnknownParameterType = false
|
|
176
|
+
reportUnknownVariableType = false
|
|
177
|
+
reportMissingTypeArgument = false
|
|
178
|
+
reportMissingTypeStubs = false
|
|
179
|
+
reportMissingImports = "none"
|
|
180
|
+
reportMissingModuleSource = "none"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "servicekit"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.1"
|
|
4
4
|
description = "Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Morten Hansen", email = "morten@dhis2.org" }]
|
|
@@ -77,6 +77,9 @@ fixable = ["ALL"]
|
|
|
77
77
|
select = ["E", "W", "F", "I", "D"]
|
|
78
78
|
ignore = ["D203", "D213"]
|
|
79
79
|
|
|
80
|
+
[tool.ruff.lint.isort]
|
|
81
|
+
known-third-party = ["alembic"]
|
|
82
|
+
|
|
80
83
|
[tool.ruff.lint.pydocstyle]
|
|
81
84
|
convention = "google"
|
|
82
85
|
|
|
@@ -11,7 +11,7 @@ except Exception:
|
|
|
11
11
|
__version__ = "unknown"
|
|
12
12
|
|
|
13
13
|
# Base infrastructure (framework-agnostic)
|
|
14
|
-
from .database import Database, SqliteDatabase, SqliteDatabaseBuilder
|
|
14
|
+
from .database import Database, SqliteDatabase, SqliteDatabaseBuilder, get_alembic_dir
|
|
15
15
|
from .exceptions import (
|
|
16
16
|
BadRequestError,
|
|
17
17
|
ConflictError,
|
|
@@ -22,6 +22,7 @@ from .exceptions import (
|
|
|
22
22
|
ServicekitException,
|
|
23
23
|
UnauthorizedError,
|
|
24
24
|
ValidationError,
|
|
25
|
+
classify_integrity_error,
|
|
25
26
|
)
|
|
26
27
|
from .logging import add_request_context, clear_request_context, configure_logging, get_logger, reset_request_context
|
|
27
28
|
from .manager import BaseManager, LifecycleHooks, Manager
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"Database",
|
|
48
49
|
"SqliteDatabase",
|
|
49
50
|
"SqliteDatabaseBuilder",
|
|
51
|
+
"get_alembic_dir",
|
|
50
52
|
"Repository",
|
|
51
53
|
"BaseRepository",
|
|
52
54
|
"Manager",
|
|
@@ -75,6 +77,7 @@ __all__ = [
|
|
|
75
77
|
"NotFoundError",
|
|
76
78
|
"ValidationError",
|
|
77
79
|
"ConflictError",
|
|
80
|
+
"classify_integrity_error",
|
|
78
81
|
"InvalidULIDError",
|
|
79
82
|
"BadRequestError",
|
|
80
83
|
"UnauthorizedError",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Alembic environment configuration for async SQLAlchemy migrations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from logging.config import fileConfig
|
|
5
|
+
|
|
6
|
+
from alembic import context
|
|
7
|
+
from sqlalchemy import pool
|
|
8
|
+
from sqlalchemy.engine import Connection
|
|
9
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
10
|
+
|
|
11
|
+
# Import the Base metadata from servicekit models
|
|
12
|
+
from servicekit.models import Base
|
|
13
|
+
|
|
14
|
+
# This is the Alembic Config object
|
|
15
|
+
config = context.config
|
|
16
|
+
|
|
17
|
+
# Interpret the config file for Python logging (if present)
|
|
18
|
+
if config.config_file_name is not None:
|
|
19
|
+
fileConfig(config.config_file_name)
|
|
20
|
+
|
|
21
|
+
# Set target metadata for 'autogenerate' support
|
|
22
|
+
target_metadata = Base.metadata
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_migrations_offline() -> None:
|
|
26
|
+
"""Run migrations in 'offline' mode (generates SQL scripts)."""
|
|
27
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
28
|
+
context.configure(
|
|
29
|
+
url=url,
|
|
30
|
+
target_metadata=target_metadata,
|
|
31
|
+
literal_binds=True,
|
|
32
|
+
dialect_opts={"paramstyle": "named"},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
with context.begin_transaction():
|
|
36
|
+
context.run_migrations()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def do_run_migrations(connection: Connection) -> None:
|
|
40
|
+
"""Run migrations with a connection."""
|
|
41
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
42
|
+
|
|
43
|
+
with context.begin_transaction():
|
|
44
|
+
context.run_migrations()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def run_async_migrations() -> None:
|
|
48
|
+
"""Run migrations in async mode using async engine."""
|
|
49
|
+
connectable = async_engine_from_config(
|
|
50
|
+
config.get_section(config.config_ini_section, {}),
|
|
51
|
+
prefix="sqlalchemy.",
|
|
52
|
+
poolclass=pool.NullPool,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async with connectable.connect() as connection:
|
|
56
|
+
await connection.run_sync(do_run_migrations)
|
|
57
|
+
|
|
58
|
+
await connectable.dispose()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run_migrations_online() -> None:
|
|
62
|
+
"""Run migrations in 'online' mode (connects to database)."""
|
|
63
|
+
# Since we're being called from Database.init() via run_in_executor,
|
|
64
|
+
# we're in a separate thread and can safely create a new event loop
|
|
65
|
+
loop = asyncio.new_event_loop()
|
|
66
|
+
asyncio.set_event_loop(loop)
|
|
67
|
+
try:
|
|
68
|
+
loop.run_until_complete(run_async_migrations())
|
|
69
|
+
finally:
|
|
70
|
+
loop.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if context.is_offline_mode():
|
|
74
|
+
run_migrations_offline()
|
|
75
|
+
else:
|
|
76
|
+
run_migrations_online()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
${imports if imports else ""}
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = ${repr(up_revision)}
|
|
14
|
+
down_revision = ${repr(down_revision)}
|
|
15
|
+
branch_labels = ${repr(branch_labels)}
|
|
16
|
+
depends_on = ${repr(depends_on)}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
"""Apply database schema changes."""
|
|
21
|
+
${upgrades if upgrades else "pass"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def downgrade() -> None:
|
|
25
|
+
"""Revert database schema changes."""
|
|
26
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Initial database schema migration - no tables for pure framework.
|
|
2
|
+
|
|
3
|
+
Note: While servicekit is a pure framework without domain tables,
|
|
4
|
+
the Entity base class includes common fields (id, created_at, updated_at, tags)
|
|
5
|
+
that all domain entities inherit. These fields are defined in the ORM models
|
|
6
|
+
and automatically included when domain tables are created.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# revision identifiers, used by Alembic.
|
|
10
|
+
revision = "4d869b5fb06e"
|
|
11
|
+
down_revision = None
|
|
12
|
+
branch_labels = None
|
|
13
|
+
depends_on = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def upgrade() -> None:
|
|
17
|
+
"""Apply database schema changes."""
|
|
18
|
+
# Servicekit is a pure framework - no domain-specific tables
|
|
19
|
+
# Domain tables (tasks, artifacts, configs) are provided by chapkit
|
|
20
|
+
# Entity base class fields (id, created_at, updated_at, tags) are
|
|
21
|
+
# automatically included in all domain entity tables via ORM inheritance
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
"""Revert database schema changes."""
|
|
27
|
+
pass
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
"""FastAPI framework layer - routers, middleware, utilities."""
|
|
2
2
|
|
|
3
|
-
from .app import App, AppInfo, AppLoader, AppManager, AppManifest
|
|
3
|
+
from .app import App, AppInfo, AppLoader, AppManager, AppManifest, EntryStaticFiles
|
|
4
4
|
from .auth import APIKeyMiddleware, load_api_keys_from_env, load_api_keys_from_file, validate_api_key_format
|
|
5
5
|
from .crud import CrudPermissions, CrudRouter
|
|
6
|
-
from .dependencies import
|
|
7
|
-
get_app_manager,
|
|
8
|
-
get_database,
|
|
9
|
-
get_scheduler,
|
|
10
|
-
get_session,
|
|
11
|
-
set_app_manager,
|
|
12
|
-
set_database,
|
|
13
|
-
set_scheduler,
|
|
14
|
-
)
|
|
6
|
+
from .dependencies import get_app_manager, get_database, get_scheduler, get_session
|
|
15
7
|
from .middleware import add_error_handlers, add_logging_middleware, database_error_handler, validation_error_handler
|
|
16
8
|
from .pagination import PaginationParams, create_paginated_response
|
|
17
9
|
from .router import Router
|
|
18
10
|
from .routers import HealthRouter, HealthState, HealthStatus, JobRouter, MetricsRouter, SystemInfo, SystemRouter
|
|
19
|
-
from .service_builder import BaseServiceBuilder, ServiceInfo
|
|
11
|
+
from .service_builder import BaseServiceBuilder, JobOptions, ServiceInfo
|
|
20
12
|
from .sse import SSE_HEADERS, format_sse_event, format_sse_model_event
|
|
21
13
|
from .utilities import build_location_url, run_app
|
|
22
14
|
|
|
@@ -27,6 +19,7 @@ __all__ = [
|
|
|
27
19
|
"CrudPermissions",
|
|
28
20
|
# Service builder
|
|
29
21
|
"BaseServiceBuilder",
|
|
22
|
+
"JobOptions",
|
|
30
23
|
"ServiceInfo",
|
|
31
24
|
# App system
|
|
32
25
|
"App",
|
|
@@ -34,6 +27,7 @@ __all__ = [
|
|
|
34
27
|
"AppLoader",
|
|
35
28
|
"AppManifest",
|
|
36
29
|
"AppManager",
|
|
30
|
+
"EntryStaticFiles",
|
|
37
31
|
# Authentication
|
|
38
32
|
"APIKeyMiddleware",
|
|
39
33
|
"load_api_keys_from_env",
|
|
@@ -41,12 +35,9 @@ __all__ = [
|
|
|
41
35
|
"validate_api_key_format",
|
|
42
36
|
# Dependencies
|
|
43
37
|
"get_app_manager",
|
|
44
|
-
"set_app_manager",
|
|
45
38
|
"get_database",
|
|
46
|
-
"set_database",
|
|
47
39
|
"get_session",
|
|
48
40
|
"get_scheduler",
|
|
49
|
-
"set_scheduler",
|
|
50
41
|
# Middleware
|
|
51
42
|
"add_error_handlers",
|
|
52
43
|
"add_logging_middleware",
|
|
@@ -6,8 +6,12 @@ import importlib.util
|
|
|
6
6
|
import json
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
9
10
|
|
|
10
11
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
12
|
+
from starlette.responses import Response
|
|
13
|
+
from starlette.staticfiles import StaticFiles
|
|
14
|
+
from starlette.types import Scope
|
|
11
15
|
|
|
12
16
|
from servicekit.logging import get_logger
|
|
13
17
|
|
|
@@ -63,6 +67,21 @@ class App:
|
|
|
63
67
|
is_package: bool # True if loaded from package resources
|
|
64
68
|
|
|
65
69
|
|
|
70
|
+
class EntryStaticFiles(StaticFiles):
|
|
71
|
+
"""Static file server that serves an app's configured entry file at the mount root."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, *args: Any, entry: str = "index.html", **kwargs: Any) -> None:
|
|
74
|
+
"""Initialize static files with the entry filename served for the mount root."""
|
|
75
|
+
super().__init__(*args, **kwargs)
|
|
76
|
+
self.entry = entry
|
|
77
|
+
|
|
78
|
+
async def get_response(self, path: str, scope: Scope) -> Response:
|
|
79
|
+
"""Serve the configured entry file for the mount root, other paths unchanged."""
|
|
80
|
+
if path in ("", "."):
|
|
81
|
+
return await super().get_response(self.entry, scope)
|
|
82
|
+
return await super().get_response(path, scope)
|
|
83
|
+
|
|
84
|
+
|
|
66
85
|
class AppLoader:
|
|
67
86
|
"""Loads and validates apps from filesystem or package resources."""
|
|
68
87
|
|