wiltech-labs-core 1.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.
@@ -0,0 +1,220 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
219
+ /NEXT_STEPS11111.md
220
+ .idea/*
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: wiltech-labs-core
3
+ Version: 1.0.1
4
+ Summary: Runtime plumbing for Wiltech FastAPI apps: config from Worker env or .env, a portable Database (SQLite, Cloudflare D1 binding, D1 over HTTP), errors, CORS and health.
5
+ Project-URL: Homepage, https://github.com/wilferraciolli/py-libraries/tree/main/packages/core
6
+ Project-URL: Repository, https://github.com/wilferraciolli/py-libraries
7
+ Project-URL: Conventions, https://github.com/wilferraciolli/py-libraries/blob/main/docs/PYTHON_APP_CONVENTIONS.md
8
+ Author: Wiliam Ferraciolli
9
+ License-Expression: Apache-2.0
10
+ Keywords: cloudflare-workers,config,d1,fastapi,sqlite
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.13
15
+ Requires-Dist: fastapi>=0.110
16
+ Requires-Dist: httpx>=0.27
17
+ Provides-Extra: sqlite
18
+ Requires-Dist: aiosqlite>=0.20; extra == 'sqlite'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # wiltech-labs-core
22
+
23
+ The runtime plumbing every Wiltech FastAPI app copies into `core/`: config that reads the
24
+ Cloudflare Worker env or `.env`, the portable `Database` protocol (SQLite locally, D1 in a
25
+ Worker, D1 over HTTP anywhere else), the `AppError` family, per-request CORS and a health
26
+ router. See
27
+ [`docs/PYTHON_APP_CONVENTIONS.md`](https://github.com/wilferraciolli/py-libraries/blob/main/docs/PYTHON_APP_CONVENTIONS.md).
28
+
29
+ Pure Python. `aiosqlite` is an extra, so a Cloudflare Worker (D1 only) installs nothing it
30
+ can't load.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ uv add "wiltech-labs-core[sqlite]" # apps that run locally / in Docker on SQLite (all of them)
36
+ uv add wiltech-labs-core # a Worker-only service that never touches SQLite
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ # main.py
43
+ from fastapi import FastAPI
44
+
45
+ from wiltech_labs_core import EnvCORSMiddleware, database_check, health_router, register_error_handlers
46
+
47
+ app = FastAPI(title="My API")
48
+ register_error_handlers(app) # AppError subclasses -> {"detail": ...} + status
49
+ app.add_middleware(EnvCORSMiddleware) # CORS_ORIGINS read per request (works in Workers)
50
+ app.include_router(health_router({"db": database_check}), prefix="/api")
51
+ ```
52
+
53
+ ```python
54
+ # todos/todo_router.py
55
+ from fastapi import Depends, Request
56
+
57
+ from wiltech_labs_core import Database, get_database
58
+
59
+
60
+ def get_todo_service(request: Request) -> TodoService:
61
+ return TodoService(TodoRepository(get_database(request)))
62
+ ```
63
+
64
+ ```python
65
+ # todos/todo_service.py
66
+ from wiltech_labs_core import NotFoundError
67
+
68
+
69
+ async def get(self, todo_id: str) -> TodoDTO:
70
+ todo = await self.repository.find(todo_id)
71
+ if todo is None:
72
+ raise NotFoundError("Todo not found") # -> 404 {"detail": "Todo not found"}
73
+ ```
74
+
75
+ ```python
76
+ # a write that must change two tables together (D1 has no interactive transactions)
77
+ await db.batch([
78
+ ("UPDATE users SET role_ids = ? WHERE id = ?", (roles, user_id)),
79
+ ("INSERT INTO audit_events (id, action, resource_id) VALUES (?, ?, ?)", (event_id, "user.roles", user_id)),
80
+ ])
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ | Variable | Used by | Default |
86
+ |---|---|---|
87
+ | `DATABASE_MODE` | `get_database` | `d1_binding` if `env.DB` exists, else `sqlite` |
88
+ | `DATABASE_PATH` | sqlite | `./local.db` |
89
+ | `MIGRATIONS_DIR` | sqlite migrations | `./migrations` (relative to the working directory) |
90
+ | `CF_ACCOUNT_ID`, `CF_D1_DATABASE_ID`, `CF_D1_API_TOKEN` | d1_http | — |
91
+ | `CORS_ORIGINS` | `EnvCORSMiddleware` | none (comma-separated) |
92
+
93
+ Values come from the Worker env (`wrangler.jsonc` vars and secrets) first, then the process
94
+ environment. `.env` in the working directory is loaded into the process environment on
95
+ import, without overriding anything already set.
96
+
97
+ ## Public API
98
+
99
+ | Name | What it is |
100
+ |---|---|
101
+ | `get_config(request, key, default=None)` | Config from the Worker env, else `os.environ` |
102
+ | `get_binding(request, name)` | A Cloudflare binding (`env.DB`, `env.AI`, `env.BUCKET`…) or `None` outside a Worker |
103
+ | `load_dotenv(path=None)` | Load `.env` (runs once on import) |
104
+ | `Database` | Protocol: `fetch_all`, `fetch_one`, `execute`, `batch` |
105
+ | `SQLiteDatabase`, `D1BindingDatabase`, `D1HttpDatabase` | The three adapters |
106
+ | `get_database(request)` | Chooses an adapter from `DATABASE_MODE` / bindings |
107
+ | `Statement` | `tuple[str, tuple]`, one item of `batch` |
108
+ | `migrations_dir()` | The migrations folder, or a `RuntimeError` if it's missing |
109
+ | `AppError`, `NotFoundError` (404), `ForbiddenError` (403), `ConflictError` (409), `InvalidInputError` (422), `TooManyRequestsError` (429 + `Retry-After`), `UpstreamError` (502), `NotConfiguredError` (503) | Business errors services raise |
110
+ | `register_error_handlers(app)` | Maps `AppError` to its status code |
111
+ | `EnvCORSMiddleware(app, default_origins="")`, `allowed_origins(scope, default="")` | CORS with origins read per request (fallback `default_origins`); unhandled errors still get CORS headers |
112
+ | `health_router(checks=None, prefix="/health")`, `HealthCheck`, `database_check` | `GET /health`; 503 if any check fails |
113
+
114
+ ### `batch` atomicity
115
+
116
+ | Adapter | Atomic? |
117
+ |---|---|
118
+ | `SQLiteDatabase` | Yes, one transaction |
119
+ | `D1BindingDatabase` | Yes, D1 `batch()` |
120
+ | `D1HttpDatabase` | **No**: one request per statement, in order |
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ uv run --package wiltech-labs-core pytest packages/core
126
+ ```
127
+
128
+ ## Publishing
129
+
130
+ Publish **before** `wiltech-labs-auth` and `wiltech-labs-ai`, which depend on it.
131
+
132
+ ```bash
133
+ cd packages/core
134
+
135
+ # 1. Test, bump, build
136
+ uv run pytest
137
+ uv version --bump patch # or minor / major; skip for the first 0.1.0
138
+ rm -rf ../../dist && uv build # -> ../../dist/
139
+
140
+ # 2. TestPyPI (optional rehearsal), using a token from test.pypi.org
141
+ read -rsp "TestPyPI token: " UV_PUBLISH_TOKEN && export UV_PUBLISH_TOKEN && echo
142
+ uv publish --index testpypi ../../dist/*
143
+ unset UV_PUBLISH_TOKEN
144
+
145
+ # 3. PyPI (the real thing), using a token from pypi.org
146
+ read -rsp "PyPI token: " UV_PUBLISH_TOKEN && export UV_PUBLISH_TOKEN && echo
147
+ uv publish ../../dist/*
148
+ unset UV_PUBLISH_TOKEN
149
+
150
+ # 4. Commit and tag
151
+ git commit -am "wiltech-labs-core <version>"
152
+ git tag core-v<version> && git push && git push --tags
153
+ ```
154
+
155
+ A version number can only be uploaded once to each site. To retry after a fix, bump the
156
+ version and rebuild.
@@ -0,0 +1,136 @@
1
+ # wiltech-labs-core
2
+
3
+ The runtime plumbing every Wiltech FastAPI app copies into `core/`: config that reads the
4
+ Cloudflare Worker env or `.env`, the portable `Database` protocol (SQLite locally, D1 in a
5
+ Worker, D1 over HTTP anywhere else), the `AppError` family, per-request CORS and a health
6
+ router. See
7
+ [`docs/PYTHON_APP_CONVENTIONS.md`](https://github.com/wilferraciolli/py-libraries/blob/main/docs/PYTHON_APP_CONVENTIONS.md).
8
+
9
+ Pure Python. `aiosqlite` is an extra, so a Cloudflare Worker (D1 only) installs nothing it
10
+ can't load.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ uv add "wiltech-labs-core[sqlite]" # apps that run locally / in Docker on SQLite (all of them)
16
+ uv add wiltech-labs-core # a Worker-only service that never touches SQLite
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ # main.py
23
+ from fastapi import FastAPI
24
+
25
+ from wiltech_labs_core import EnvCORSMiddleware, database_check, health_router, register_error_handlers
26
+
27
+ app = FastAPI(title="My API")
28
+ register_error_handlers(app) # AppError subclasses -> {"detail": ...} + status
29
+ app.add_middleware(EnvCORSMiddleware) # CORS_ORIGINS read per request (works in Workers)
30
+ app.include_router(health_router({"db": database_check}), prefix="/api")
31
+ ```
32
+
33
+ ```python
34
+ # todos/todo_router.py
35
+ from fastapi import Depends, Request
36
+
37
+ from wiltech_labs_core import Database, get_database
38
+
39
+
40
+ def get_todo_service(request: Request) -> TodoService:
41
+ return TodoService(TodoRepository(get_database(request)))
42
+ ```
43
+
44
+ ```python
45
+ # todos/todo_service.py
46
+ from wiltech_labs_core import NotFoundError
47
+
48
+
49
+ async def get(self, todo_id: str) -> TodoDTO:
50
+ todo = await self.repository.find(todo_id)
51
+ if todo is None:
52
+ raise NotFoundError("Todo not found") # -> 404 {"detail": "Todo not found"}
53
+ ```
54
+
55
+ ```python
56
+ # a write that must change two tables together (D1 has no interactive transactions)
57
+ await db.batch([
58
+ ("UPDATE users SET role_ids = ? WHERE id = ?", (roles, user_id)),
59
+ ("INSERT INTO audit_events (id, action, resource_id) VALUES (?, ?, ?)", (event_id, "user.roles", user_id)),
60
+ ])
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ | Variable | Used by | Default |
66
+ |---|---|---|
67
+ | `DATABASE_MODE` | `get_database` | `d1_binding` if `env.DB` exists, else `sqlite` |
68
+ | `DATABASE_PATH` | sqlite | `./local.db` |
69
+ | `MIGRATIONS_DIR` | sqlite migrations | `./migrations` (relative to the working directory) |
70
+ | `CF_ACCOUNT_ID`, `CF_D1_DATABASE_ID`, `CF_D1_API_TOKEN` | d1_http | — |
71
+ | `CORS_ORIGINS` | `EnvCORSMiddleware` | none (comma-separated) |
72
+
73
+ Values come from the Worker env (`wrangler.jsonc` vars and secrets) first, then the process
74
+ environment. `.env` in the working directory is loaded into the process environment on
75
+ import, without overriding anything already set.
76
+
77
+ ## Public API
78
+
79
+ | Name | What it is |
80
+ |---|---|
81
+ | `get_config(request, key, default=None)` | Config from the Worker env, else `os.environ` |
82
+ | `get_binding(request, name)` | A Cloudflare binding (`env.DB`, `env.AI`, `env.BUCKET`…) or `None` outside a Worker |
83
+ | `load_dotenv(path=None)` | Load `.env` (runs once on import) |
84
+ | `Database` | Protocol: `fetch_all`, `fetch_one`, `execute`, `batch` |
85
+ | `SQLiteDatabase`, `D1BindingDatabase`, `D1HttpDatabase` | The three adapters |
86
+ | `get_database(request)` | Chooses an adapter from `DATABASE_MODE` / bindings |
87
+ | `Statement` | `tuple[str, tuple]`, one item of `batch` |
88
+ | `migrations_dir()` | The migrations folder, or a `RuntimeError` if it's missing |
89
+ | `AppError`, `NotFoundError` (404), `ForbiddenError` (403), `ConflictError` (409), `InvalidInputError` (422), `TooManyRequestsError` (429 + `Retry-After`), `UpstreamError` (502), `NotConfiguredError` (503) | Business errors services raise |
90
+ | `register_error_handlers(app)` | Maps `AppError` to its status code |
91
+ | `EnvCORSMiddleware(app, default_origins="")`, `allowed_origins(scope, default="")` | CORS with origins read per request (fallback `default_origins`); unhandled errors still get CORS headers |
92
+ | `health_router(checks=None, prefix="/health")`, `HealthCheck`, `database_check` | `GET /health`; 503 if any check fails |
93
+
94
+ ### `batch` atomicity
95
+
96
+ | Adapter | Atomic? |
97
+ |---|---|
98
+ | `SQLiteDatabase` | Yes, one transaction |
99
+ | `D1BindingDatabase` | Yes, D1 `batch()` |
100
+ | `D1HttpDatabase` | **No**: one request per statement, in order |
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ uv run --package wiltech-labs-core pytest packages/core
106
+ ```
107
+
108
+ ## Publishing
109
+
110
+ Publish **before** `wiltech-labs-auth` and `wiltech-labs-ai`, which depend on it.
111
+
112
+ ```bash
113
+ cd packages/core
114
+
115
+ # 1. Test, bump, build
116
+ uv run pytest
117
+ uv version --bump patch # or minor / major; skip for the first 0.1.0
118
+ rm -rf ../../dist && uv build # -> ../../dist/
119
+
120
+ # 2. TestPyPI (optional rehearsal), using a token from test.pypi.org
121
+ read -rsp "TestPyPI token: " UV_PUBLISH_TOKEN && export UV_PUBLISH_TOKEN && echo
122
+ uv publish --index testpypi ../../dist/*
123
+ unset UV_PUBLISH_TOKEN
124
+
125
+ # 3. PyPI (the real thing), using a token from pypi.org
126
+ read -rsp "PyPI token: " UV_PUBLISH_TOKEN && export UV_PUBLISH_TOKEN && echo
127
+ uv publish ../../dist/*
128
+ unset UV_PUBLISH_TOKEN
129
+
130
+ # 4. Commit and tag
131
+ git commit -am "wiltech-labs-core <version>"
132
+ git tag core-v<version> && git push && git push --tags
133
+ ```
134
+
135
+ A version number can only be uploaded once to each site. To retry after a fix, bump the
136
+ version and rebuild.
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "wiltech-labs-core"
3
+ version = "1.0.1"
4
+ description = "Runtime plumbing for Wiltech FastAPI apps: config from Worker env or .env, a portable Database (SQLite, Cloudflare D1 binding, D1 over HTTP), errors, CORS and health."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.13"
8
+ authors = [{ name = "Wiliam Ferraciolli" }]
9
+ keywords = ["fastapi", "cloudflare-workers", "d1", "sqlite", "config"]
10
+ classifiers = [
11
+ "Framework :: FastAPI",
12
+ "Programming Language :: Python :: 3",
13
+ "Typing :: Typed",
14
+ ]
15
+ dependencies = [
16
+ "fastapi>=0.110",
17
+ "httpx>=0.27",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ # Local uvicorn/Docker only. A Cloudflare Worker uses D1 and must not need it.
22
+ sqlite = ["aiosqlite>=0.20"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/wilferraciolli/py-libraries/tree/main/packages/core"
26
+ Repository = "https://github.com/wilferraciolli/py-libraries"
27
+ Conventions = "https://github.com/wilferraciolli/py-libraries/blob/main/docs/PYTHON_APP_CONVENTIONS.md"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "aiosqlite",
32
+ "pytest",
33
+ "pytest-asyncio",
34
+ ]
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ exclude = ["CLAUDE.md", ".pytest_cache"]
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/wiltech_labs_core"]
45
+
46
+ [tool.pytest.ini_options]
47
+ asyncio_mode = "auto"
48
+ testpaths = ["tests"]
@@ -0,0 +1,59 @@
1
+ """
2
+ wiltech-labs-core: the runtime plumbing shared by Wiltech's FastAPI apps.
3
+
4
+ Config that reads the Worker env or .env, the portable Database protocol
5
+ (SQLite / D1 binding / D1 over HTTP), the AppError family, per-request CORS
6
+ and a health router.
7
+
8
+ This module is the entire public surface — import from `wiltech_labs_core`,
9
+ not from its sub-packages.
10
+ """
11
+ from wiltech_labs_core.config import get_binding, get_config, load_dotenv
12
+ from wiltech_labs_core.database import (
13
+ D1BindingDatabase,
14
+ D1HttpDatabase,
15
+ Database,
16
+ SQLiteDatabase,
17
+ Statement,
18
+ get_database,
19
+ migrations_dir,
20
+ )
21
+ from wiltech_labs_core.errors import (
22
+ AppError,
23
+ ConflictError,
24
+ ForbiddenError,
25
+ InvalidInputError,
26
+ NotConfiguredError,
27
+ NotFoundError,
28
+ TooManyRequestsError,
29
+ UpstreamError,
30
+ register_error_handlers,
31
+ )
32
+ from wiltech_labs_core.web import EnvCORSMiddleware, HealthCheck, allowed_origins, database_check, health_router
33
+
34
+ __all__ = [
35
+ "AppError",
36
+ "ConflictError",
37
+ "D1BindingDatabase",
38
+ "D1HttpDatabase",
39
+ "Database",
40
+ "EnvCORSMiddleware",
41
+ "ForbiddenError",
42
+ "HealthCheck",
43
+ "InvalidInputError",
44
+ "NotConfiguredError",
45
+ "NotFoundError",
46
+ "SQLiteDatabase",
47
+ "Statement",
48
+ "TooManyRequestsError",
49
+ "UpstreamError",
50
+ "allowed_origins",
51
+ "database_check",
52
+ "get_binding",
53
+ "get_config",
54
+ "get_database",
55
+ "health_router",
56
+ "load_dotenv",
57
+ "migrations_dir",
58
+ "register_error_handlers",
59
+ ]
@@ -0,0 +1,3 @@
1
+ from wiltech_labs_core.config.config import get_binding, get_config, load_dotenv
2
+
3
+ __all__ = ["get_binding", "get_config", "load_dotenv"]
@@ -0,0 +1,60 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from starlette.requests import HTTPConnection
6
+
7
+
8
+ def load_dotenv(path: Optional[Path] = None) -> None:
9
+ """
10
+ Load the project's .env (in the working directory) for plain uvicorn/local runs, if one exists.
11
+
12
+ Values already in the environment win, so docker-compose `environment:`
13
+ entries override the file. Runs once on import; call it again only to load
14
+ a different file.
15
+ """
16
+ env_path = path or Path.cwd() / ".env"
17
+ if not env_path.exists():
18
+ return
19
+
20
+ for line in env_path.read_text().splitlines():
21
+ line = line.strip()
22
+ if not line or line.startswith("#") or "=" not in line:
23
+ continue
24
+
25
+ key, value = line.split("=", 1)
26
+ os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
27
+
28
+
29
+ load_dotenv()
30
+
31
+
32
+ def get_config(request: HTTPConnection, key: str, default: Optional[str] = None) -> Optional[str]:
33
+ """
34
+ Read config from either the Worker env object or process environment.
35
+
36
+ Cloudflare injects bindings/config into request.scope["env"], while plain
37
+ uvicorn/Docker uses environment variables loaded from .env/docker-compose.
38
+ """
39
+ env = request.scope.get("env")
40
+ if env is not None:
41
+ try:
42
+ value = getattr(env, key)
43
+ if value is not None:
44
+ return str(value)
45
+ except AttributeError:
46
+ pass
47
+
48
+ return os.environ.get(key, default)
49
+
50
+
51
+ def get_binding(request: HTTPConnection, name: str) -> Optional[object]:
52
+ """
53
+ A Cloudflare binding (env.DB, env.AI, env.BUCKET, ...) or None outside a
54
+ Worker. Adapter factories use this to prefer the native binding when it
55
+ exists and fall back to HTTP otherwise.
56
+ """
57
+ env = request.scope.get("env")
58
+ if env is None:
59
+ return None
60
+ return getattr(env, name, None)