causeway 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 (43) hide show
  1. causeway-0.1.0/.gitignore +93 -0
  2. causeway-0.1.0/CHANGELOG.md +29 -0
  3. causeway-0.1.0/PKG-INFO +146 -0
  4. causeway-0.1.0/README.md +94 -0
  5. causeway-0.1.0/pyproject.toml +159 -0
  6. causeway-0.1.0/src/causeway/__init__.py +68 -0
  7. causeway-0.1.0/src/causeway/_cron.py +65 -0
  8. causeway-0.1.0/src/causeway/_methods.py +42 -0
  9. causeway-0.1.0/src/causeway/_paths.py +116 -0
  10. causeway-0.1.0/src/causeway/_scaffold.py +261 -0
  11. causeway-0.1.0/src/causeway/adapters.py +305 -0
  12. causeway-0.1.0/src/causeway/app.py +98 -0
  13. causeway-0.1.0/src/causeway/cli.py +227 -0
  14. causeway-0.1.0/src/causeway/config.py +107 -0
  15. causeway-0.1.0/src/causeway/contracts.py +218 -0
  16. causeway-0.1.0/src/causeway/diagnostics.py +93 -0
  17. causeway-0.1.0/src/causeway/errors.py +135 -0
  18. causeway-0.1.0/src/causeway/health.py +54 -0
  19. causeway-0.1.0/src/causeway/middleware.py +53 -0
  20. causeway-0.1.0/src/causeway/observability.py +142 -0
  21. causeway-0.1.0/src/causeway/plugins.py +235 -0
  22. causeway-0.1.0/src/causeway/py.typed +0 -0
  23. causeway-0.1.0/src/causeway/routing.py +360 -0
  24. causeway-0.1.0/src/causeway/scope.py +41 -0
  25. causeway-0.1.0/src/causeway/tasks.py +328 -0
  26. causeway-0.1.0/src/causeway/testing.py +124 -0
  27. causeway-0.1.0/tests/__init__.py +0 -0
  28. causeway-0.1.0/tests/test_adapters.py +189 -0
  29. causeway-0.1.0/tests/test_app_factory.py +87 -0
  30. causeway-0.1.0/tests/test_cli.py +49 -0
  31. causeway-0.1.0/tests/test_config.py +58 -0
  32. causeway-0.1.0/tests/test_errors.py +68 -0
  33. causeway-0.1.0/tests/test_health.py +76 -0
  34. causeway-0.1.0/tests/test_observability.py +63 -0
  35. causeway-0.1.0/tests/test_paths.py +73 -0
  36. causeway-0.1.0/tests/test_plugin_features.py +109 -0
  37. causeway-0.1.0/tests/test_plugins.py +152 -0
  38. causeway-0.1.0/tests/test_provider_binding.py +89 -0
  39. causeway-0.1.0/tests/test_routing_discover.py +154 -0
  40. causeway-0.1.0/tests/test_routing_register.py +104 -0
  41. causeway-0.1.0/tests/test_tasks.py +158 -0
  42. causeway-0.1.0/tests/test_testing_kit.py +63 -0
  43. causeway-0.1.0/tests/test_version.py +8 -0
@@ -0,0 +1,93 @@
1
+ # ---- OS ----
2
+ .DS_Store
3
+ Thumbs.db
4
+ desktop.ini
5
+
6
+ # ---- Editors ----
7
+ .idea/
8
+ *.swp
9
+ *.swo
10
+ *~
11
+ .vscode/*
12
+ !.vscode/settings.json
13
+ !.vscode/extensions.json
14
+ !.vscode/launch.json
15
+
16
+ # ---- Node ----
17
+ node_modules/
18
+ .pnp.*
19
+ .yarn/*
20
+ !.yarn/patches
21
+ !.yarn/plugins
22
+ !.yarn/releases
23
+ !.yarn/sdks
24
+ !.yarn/versions
25
+ .npm/
26
+ .eslintcache
27
+ *.tsbuildinfo
28
+
29
+ # ---- Build output ----
30
+ dist/
31
+ build/
32
+ out/
33
+ .turbo/
34
+ .cache/
35
+ coverage/
36
+ *.log
37
+ npm-debug.log*
38
+ yarn-debug.log*
39
+ yarn-error.log*
40
+ pnpm-debug.log*
41
+
42
+ # ---- Python ----
43
+ __pycache__/
44
+ *.py[cod]
45
+ *$py.class
46
+ *.so
47
+ .Python
48
+ .python-version-local
49
+ .venv/
50
+ venv/
51
+ env/
52
+ ENV/
53
+ .uv-cache/
54
+ .eggs/
55
+ *.egg-info/
56
+ *.egg
57
+ .pytest_cache/
58
+ .mypy_cache/
59
+ .ruff_cache/
60
+ .coverage
61
+ .coverage.*
62
+ htmlcov/
63
+ .tox/
64
+ .nox/
65
+ .hypothesis/
66
+ *.cover
67
+ *.py,cover
68
+
69
+ # ---- Claude Code harness ----
70
+ .claude/
71
+ !.claude/settings.json
72
+ !.claude/commands/
73
+
74
+ # ---- Causeway ----
75
+ .causeway/
76
+
77
+ # ---- Benchmarks ----
78
+ benchmarks/results/
79
+ benchmarks/uv.lock
80
+
81
+ # ---- Env / secrets ----
82
+ .env
83
+ .env.*
84
+ !.env.example
85
+ *.pem
86
+ *.key
87
+ *.crt
88
+
89
+ # ---- Misc ----
90
+ *.tgz
91
+ *.zip
92
+ .DS_Store?
93
+ ._*
@@ -0,0 +1,29 @@
1
+ # Changelog · `causeway`
2
+
3
+ All notable changes to the `causeway` Python package will be documented in this
4
+ file. Managed automatically by [release-please](https://github.com/googleapis/release-please)
5
+ from [Conventional Commits](https://www.conventionalcommits.org/).
6
+
7
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
8
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
+
10
+ ## 0.1.0 (2026-05-16)
11
+
12
+
13
+ ### ⚠ BREAKING CHANGES
14
+
15
+ * drops the 0.1.0a0 pre-release line. The first published versions on PyPI will be 0.1.0.
16
+ * the framework is now distributed as `causeway` (and `causeway-<role>-<impl>` for plugins). Every public surface moves:
17
+
18
+ ### Chores
19
+
20
+ * prep first 0.1.0 release across all 13 packages ([11ef338](https://github.com/tamimbinhakim/causeway/commit/11ef3382a6263cbe541455dd169a28cf162ebb3d))
21
+ * rename quay → causeway across the repo ([83f1213](https://github.com/tamimbinhakim/causeway/commit/83f1213a2176e8338305f8b30f43379d6a614238))
22
+
23
+ ## [Unreleased]
24
+
25
+ ### Added
26
+
27
+ - Initial package scaffold: file-based router, typed config / DI,
28
+ middleware + scope composition, background-task contract + reference
29
+ adapter, plugin registry, `causeway` CLI.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: causeway
3
+ Version: 0.1.0
4
+ Summary: A lean backend framework for type-safe Python APIs — file-based routing, typed config/DI, scoped providers, plugin registry, and background tasks.
5
+ Project-URL: Homepage, https://github.com/tamimbinhakim/causeway
6
+ Project-URL: Documentation, https://github.com/tamimbinhakim/causeway/tree/main/docs
7
+ Project-URL: Repository, https://github.com/tamimbinhakim/causeway
8
+ Project-URL: Issues, https://github.com/tamimbinhakim/causeway/issues
9
+ Project-URL: Changelog, https://github.com/tamimbinhakim/causeway/blob/main/packages/causeway/CHANGELOG.md
10
+ Author-email: Tamim Bin Hakim <tamimbinhakim.work@gmail.com>
11
+ License: MIT
12
+ Keywords: asgi,backend-framework,background-tasks,dependency-injection,dyadpy,file-based-routing
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: anyio>=4.13.0
28
+ Requires-Dist: dyadpy>=0.1.0a0
29
+ Requires-Dist: msgspec>=0.19
30
+ Requires-Dist: pydantic-settings>=2.14.1
31
+ Requires-Dist: pydantic>=2.13.4
32
+ Requires-Dist: rich>=15.0.0
33
+ Requires-Dist: starlette>=1.0.0
34
+ Requires-Dist: structlog>=25.5.0
35
+ Requires-Dist: typer>=0.25.1
36
+ Requires-Dist: uvicorn[standard]>=0.47.0
37
+ Requires-Dist: watchfiles>=1.1.1
38
+ Provides-Extra: all
39
+ Requires-Dist: dramatiq[redis]>=2.1.0; extra == 'all'
40
+ Requires-Dist: opentelemetry-api>=1.41.1; extra == 'all'
41
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.41.1; extra == 'all'
42
+ Requires-Dist: opentelemetry-instrumentation-asgi>=0.62b1; extra == 'all'
43
+ Requires-Dist: opentelemetry-sdk>=1.41.1; extra == 'all'
44
+ Provides-Extra: dramatiq
45
+ Requires-Dist: dramatiq[redis]>=2.1.0; extra == 'dramatiq'
46
+ Provides-Extra: otel
47
+ Requires-Dist: opentelemetry-api>=1.41.1; extra == 'otel'
48
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.41.1; extra == 'otel'
49
+ Requires-Dist: opentelemetry-instrumentation-asgi>=0.62b1; extra == 'otel'
50
+ Requires-Dist: opentelemetry-sdk>=1.41.1; extra == 'otel'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # causeway (Python)
54
+
55
+ > A lean backend framework for type-safe Python APIs.
56
+
57
+ ```bash
58
+ uv add 'causeway==0.1.0a0' # alpha — drop the pin once v0.1.0 ships
59
+ ```
60
+
61
+ This is the core Python package of [Causeway](https://github.com/tamimbinhakim/causeway). It ships:
62
+
63
+ - A **file-based router** that walks `src/app/routes/**/*.py`, picks up `[id].py`, `(group)/`, `_middleware.py`, and `_scope.py`, and registers handlers into the app's route table.
64
+ - A **typed config layer** that wraps `pydantic-settings` and exposes non-secret fields to the generated TypeScript client.
65
+ - A **scoped DI container** driven by `_scope.py` providers.
66
+ - A **`@task` contract** for background jobs with a Dramatiq reference adapter.
67
+ - A **plugin registry** with Python-entry-point discovery and per-environment activation.
68
+ - A **CLI** (`causeway new`, `causeway dev`, `causeway build`, `causeway deploy <target>`) that runs the whole loop.
69
+
70
+ For the full story, the design rationale, and a side-by-side vs. FastAPI / Django + Ninja / Encore.ts / NestJS, see the [repo README](https://github.com/tamimbinhakim/causeway).
71
+
72
+ ## 30-second example
73
+
74
+ ```
75
+ my-app/
76
+ ├── pyproject.toml
77
+ ├── causeway.toml
78
+ └── src/app/
79
+ ├── config.py
80
+ ├── plugins.py
81
+ └── routes/
82
+ ├── _middleware.py
83
+ ├── index.py
84
+ └── users/
85
+ ├── _scope.py
86
+ ├── index.py
87
+ └── [id].py
88
+ ```
89
+
90
+ ```python
91
+ # src/app/routes/users/[id].py
92
+ from typing import Annotated
93
+ from uuid import UUID
94
+ from msgspec import Struct
95
+ from causeway import get, raises
96
+ from causeway.errors import NotFound
97
+
98
+ class User(Struct):
99
+ id: UUID
100
+ name: str
101
+
102
+ @get
103
+ @raises(NotFound)
104
+ async def show(id: UUID) -> User:
105
+ ...
106
+ ```
107
+
108
+ Run it:
109
+
110
+ ```bash
111
+ causeway dev
112
+ ```
113
+
114
+ `causeway dev` discovers the routes, boots uvicorn, regenerates a typed `client.ts` for your frontend on every save, and exposes `/__causeway` with the route tree, registered tasks, current config, and plugin list.
115
+
116
+ ## Primitives in this package
117
+
118
+ | Primitive | Purpose |
119
+ | ------------------------------------------------ | ------------------------------------------------------------------------ |
120
+ | File router (`[id].py`, `(group)/`) | Discovery + URL pattern generation. |
121
+ | `_middleware.py` / `_scope.py` | Per-subtree middleware + scoped DI providers. |
122
+ | `Settings` (wraps `pydantic-settings`) | Typed config with allowlisted exposure to the generated client. |
123
+ | `@get` / `@post` / `@put` / `@patch` / `@delete` | HTTP method decorators. |
124
+ | `@task(...)` | Background-job contract; adapter-agnostic. |
125
+ | `@cron(...)` | Scheduled tasks via the same adapter. |
126
+ | `register(...)` | Plugin registration (`TaskAdapter`, `Storage`, `KV`, `AuthProvider`, …). |
127
+ | `TestApp` | Test client with DI overrides and `tasks_eager()` mode. |
128
+ | `causeway` CLI | `new`, `dev`, `build`, `plugins`, `deploy <target>`. |
129
+
130
+ Full reference: <https://github.com/tamimbinhakim/causeway/blob/main/docs/reference.md>
131
+
132
+ ## Optional extras
133
+
134
+ ```bash
135
+ uv add 'causeway[dramatiq]' # reference task adapter
136
+ uv add 'causeway[otel]' # OpenTelemetry middleware hooks
137
+ uv add 'causeway[all]' # everything
138
+ ```
139
+
140
+ ## Scope
141
+
142
+ Causeway ships project shape and a small set of contracts. It does **not** ship vertical integrations — no ORM, no admin panel, no template engine, no infrastructure provisioning. Those layers compose on top of the fundamentals and live in their own plugin packages or in user code.
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,94 @@
1
+ # causeway (Python)
2
+
3
+ > A lean backend framework for type-safe Python APIs.
4
+
5
+ ```bash
6
+ uv add 'causeway==0.1.0a0' # alpha — drop the pin once v0.1.0 ships
7
+ ```
8
+
9
+ This is the core Python package of [Causeway](https://github.com/tamimbinhakim/causeway). It ships:
10
+
11
+ - A **file-based router** that walks `src/app/routes/**/*.py`, picks up `[id].py`, `(group)/`, `_middleware.py`, and `_scope.py`, and registers handlers into the app's route table.
12
+ - A **typed config layer** that wraps `pydantic-settings` and exposes non-secret fields to the generated TypeScript client.
13
+ - A **scoped DI container** driven by `_scope.py` providers.
14
+ - A **`@task` contract** for background jobs with a Dramatiq reference adapter.
15
+ - A **plugin registry** with Python-entry-point discovery and per-environment activation.
16
+ - A **CLI** (`causeway new`, `causeway dev`, `causeway build`, `causeway deploy <target>`) that runs the whole loop.
17
+
18
+ For the full story, the design rationale, and a side-by-side vs. FastAPI / Django + Ninja / Encore.ts / NestJS, see the [repo README](https://github.com/tamimbinhakim/causeway).
19
+
20
+ ## 30-second example
21
+
22
+ ```
23
+ my-app/
24
+ ├── pyproject.toml
25
+ ├── causeway.toml
26
+ └── src/app/
27
+ ├── config.py
28
+ ├── plugins.py
29
+ └── routes/
30
+ ├── _middleware.py
31
+ ├── index.py
32
+ └── users/
33
+ ├── _scope.py
34
+ ├── index.py
35
+ └── [id].py
36
+ ```
37
+
38
+ ```python
39
+ # src/app/routes/users/[id].py
40
+ from typing import Annotated
41
+ from uuid import UUID
42
+ from msgspec import Struct
43
+ from causeway import get, raises
44
+ from causeway.errors import NotFound
45
+
46
+ class User(Struct):
47
+ id: UUID
48
+ name: str
49
+
50
+ @get
51
+ @raises(NotFound)
52
+ async def show(id: UUID) -> User:
53
+ ...
54
+ ```
55
+
56
+ Run it:
57
+
58
+ ```bash
59
+ causeway dev
60
+ ```
61
+
62
+ `causeway dev` discovers the routes, boots uvicorn, regenerates a typed `client.ts` for your frontend on every save, and exposes `/__causeway` with the route tree, registered tasks, current config, and plugin list.
63
+
64
+ ## Primitives in this package
65
+
66
+ | Primitive | Purpose |
67
+ | ------------------------------------------------ | ------------------------------------------------------------------------ |
68
+ | File router (`[id].py`, `(group)/`) | Discovery + URL pattern generation. |
69
+ | `_middleware.py` / `_scope.py` | Per-subtree middleware + scoped DI providers. |
70
+ | `Settings` (wraps `pydantic-settings`) | Typed config with allowlisted exposure to the generated client. |
71
+ | `@get` / `@post` / `@put` / `@patch` / `@delete` | HTTP method decorators. |
72
+ | `@task(...)` | Background-job contract; adapter-agnostic. |
73
+ | `@cron(...)` | Scheduled tasks via the same adapter. |
74
+ | `register(...)` | Plugin registration (`TaskAdapter`, `Storage`, `KV`, `AuthProvider`, …). |
75
+ | `TestApp` | Test client with DI overrides and `tasks_eager()` mode. |
76
+ | `causeway` CLI | `new`, `dev`, `build`, `plugins`, `deploy <target>`. |
77
+
78
+ Full reference: <https://github.com/tamimbinhakim/causeway/blob/main/docs/reference.md>
79
+
80
+ ## Optional extras
81
+
82
+ ```bash
83
+ uv add 'causeway[dramatiq]' # reference task adapter
84
+ uv add 'causeway[otel]' # OpenTelemetry middleware hooks
85
+ uv add 'causeway[all]' # everything
86
+ ```
87
+
88
+ ## Scope
89
+
90
+ Causeway ships project shape and a small set of contracts. It does **not** ship vertical integrations — no ORM, no admin panel, no template engine, no infrastructure provisioning. Those layers compose on top of the fundamentals and live in their own plugin packages or in user code.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,159 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.29.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "causeway"
7
+ version = "0.1.0"
8
+ description = "A lean backend framework for type-safe Python APIs — file-based routing, typed config/DI, scoped providers, plugin registry, and background tasks."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Tamim Bin Hakim", email = "tamimbinhakim.work@gmail.com" }]
13
+ keywords = [
14
+ "asgi",
15
+ "backend-framework",
16
+ "file-based-routing",
17
+ "dependency-injection",
18
+ "background-tasks",
19
+ "dyadpy",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Framework :: AsyncIO",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Topic :: Internet :: WWW/HTTP",
33
+ "Topic :: Software Development :: Libraries :: Python Modules",
34
+ "Typing :: Typed",
35
+ ]
36
+
37
+ dependencies = [
38
+ "dyadpy>=0.1.0a0",
39
+ "pydantic-settings>=2.14.1",
40
+ "pydantic>=2.13.4",
41
+ "msgspec>=0.19",
42
+ "starlette>=1.0.0",
43
+ "uvicorn[standard]>=0.47.0",
44
+ "watchfiles>=1.1.1",
45
+ "typer>=0.25.1",
46
+ "rich>=15.0.0",
47
+ "structlog>=25.5.0",
48
+ "anyio>=4.13.0",
49
+ ]
50
+
51
+ [project.optional-dependencies]
52
+ dramatiq = ["dramatiq[redis]>=2.1.0"]
53
+ otel = [
54
+ "opentelemetry-api>=1.41.1",
55
+ "opentelemetry-sdk>=1.41.1",
56
+ "opentelemetry-instrumentation-asgi>=0.62b1",
57
+ "opentelemetry-exporter-otlp-proto-http>=1.41.1",
58
+ ]
59
+ all = ["causeway[dramatiq,otel]"]
60
+
61
+ [project.scripts]
62
+ causeway = "causeway.cli:main"
63
+
64
+ [project.entry-points."causeway.plugins"]
65
+ # Reference adapters shipped in core register themselves under this group.
66
+
67
+ [project.urls]
68
+ Homepage = "https://github.com/tamimbinhakim/causeway"
69
+ Documentation = "https://github.com/tamimbinhakim/causeway/tree/main/docs"
70
+ Repository = "https://github.com/tamimbinhakim/causeway"
71
+ Issues = "https://github.com/tamimbinhakim/causeway/issues"
72
+ Changelog = "https://github.com/tamimbinhakim/causeway/blob/main/packages/causeway/CHANGELOG.md"
73
+
74
+ [dependency-groups]
75
+ dev = [
76
+ "pytest>=9.0.3",
77
+ "pytest-asyncio>=1.3.0",
78
+ "pytest-cov>=7.1.0",
79
+ "httpx>=0.28.1",
80
+ "ruff>=0.15.13",
81
+ "mypy>=2.1.0",
82
+ "types-setuptools",
83
+ ]
84
+
85
+ [tool.hatch.build.targets.wheel]
86
+ packages = ["src/causeway"]
87
+
88
+ [tool.hatch.build.targets.sdist]
89
+ include = ["/src", "/tests", "/README.md", "/CHANGELOG.md"]
90
+
91
+ # ---- ruff ----
92
+ [tool.ruff]
93
+ line-length = 100
94
+ target-version = "py311"
95
+ src = ["src", "tests"]
96
+
97
+ [tool.ruff.lint]
98
+ select = [
99
+ "E",
100
+ "F",
101
+ "W", # pycodestyle / pyflakes
102
+ "I", # isort
103
+ "B", # bugbear
104
+ "UP", # pyupgrade
105
+ "SIM", # simplify
106
+ "C4", # comprehensions
107
+ "PT", # pytest
108
+ "RUF", # ruff
109
+ "TID", # tidy imports
110
+ "PYI", # type stubs
111
+ "ANN", # annotations
112
+ ]
113
+ ignore = [
114
+ "ANN401", # allow Any in select cases
115
+ "E501", # line length handled by formatter
116
+ ]
117
+
118
+ [tool.ruff.lint.per-file-ignores]
119
+ "tests/**" = ["ANN", "B017", "PT011", "RUF012"]
120
+
121
+ [tool.ruff.lint.isort]
122
+ known-first-party = ["causeway"]
123
+
124
+ [tool.ruff.format]
125
+ quote-style = "double"
126
+ indent-style = "space"
127
+ docstring-code-format = true
128
+
129
+ # ---- mypy ----
130
+ [tool.mypy]
131
+ python_version = "3.11"
132
+ strict = true
133
+ warn_unreachable = true
134
+ warn_redundant_casts = true
135
+ warn_unused_ignores = true
136
+ disallow_any_generics = true
137
+ no_implicit_reexport = true
138
+ files = ["src/causeway"]
139
+
140
+ [[tool.mypy.overrides]]
141
+ module = "tests.*"
142
+ disallow_untyped_defs = false
143
+
144
+ # ---- pytest ----
145
+ [tool.pytest.ini_options]
146
+ minversion = "8.0"
147
+ testpaths = ["tests"]
148
+ asyncio_mode = "auto"
149
+ addopts = ["-ra", "--strict-markers", "--strict-config"]
150
+ filterwarnings = ["error"]
151
+
152
+ # ---- coverage ----
153
+ [tool.coverage.run]
154
+ branch = true
155
+ source = ["causeway"]
156
+ omit = ["src/causeway/cli.py"]
157
+
158
+ [tool.coverage.report]
159
+ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", "\\.\\.\\."]
@@ -0,0 +1,68 @@
1
+ """Causeway — a lean backend framework for type-safe Python APIs.
2
+
3
+ Public API surface for v0.1. Anything not listed in ``__all__`` (or imported
4
+ from a private underscore module like ``causeway._methods``) is implementation
5
+ detail and may change in any release. See ``docs/semver.md``.
6
+
7
+ Slice landed in this release:
8
+
9
+ - File-based routing primitives — :func:`get` / :func:`post` / :func:`put` /
10
+ :func:`patch` / :func:`delete` decorators, :func:`causeway.routing.discover` and
11
+ :func:`causeway.routing.register` for the walker.
12
+ - Scopes — :func:`provide` for ``_scope.py`` providers.
13
+ - Middleware — :class:`Middleware` base class and :func:`guard` decorator.
14
+ - Config — :class:`Settings` (re-export of ``pydantic_settings.BaseSettings``)
15
+ and :class:`Manifest` (parsed ``causeway.toml``).
16
+
17
+ Re-exported from ``dyadpy`` so app code only depends on ``causeway``:
18
+
19
+ - :func:`Depends` — DI marker for handler parameters.
20
+ - :func:`raises` — declare exception types the handler may raise (flows to
21
+ the generated TS client's ``Result<T, E>`` union).
22
+ - :func:`stream` — SSE return type marker.
23
+ - :data:`Bytes` — raw-body sentinel.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dyadpy import Bytes, Depends, raises, stream
29
+
30
+ from causeway import errors
31
+ from causeway._methods import delete, get, patch, post, put
32
+ from causeway.app import create_app
33
+ from causeway.config import Manifest, Settings
34
+ from causeway.middleware import Middleware, guard
35
+ from causeway.observability import RequestIdMiddleware, configure_logging, configure_otel
36
+ from causeway.plugins import env, register
37
+ from causeway.scope import provide
38
+ from causeway.tasks import cron, task, tasks_eager
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ __all__ = [
43
+ "Bytes",
44
+ "Depends",
45
+ "Manifest",
46
+ "Middleware",
47
+ "RequestIdMiddleware",
48
+ "Settings",
49
+ "__version__",
50
+ "configure_logging",
51
+ "configure_otel",
52
+ "create_app",
53
+ "cron",
54
+ "delete",
55
+ "env",
56
+ "errors",
57
+ "get",
58
+ "guard",
59
+ "patch",
60
+ "post",
61
+ "provide",
62
+ "put",
63
+ "raises",
64
+ "register",
65
+ "stream",
66
+ "task",
67
+ "tasks_eager",
68
+ ]
@@ -0,0 +1,65 @@
1
+ """Tiny crontab parser.
2
+
3
+ Five fields: minute hour day-of-month month day-of-week. Each field is one
4
+ of: ``*``, an integer, a comma list ``1,3,5``, or a step ``*/N``. That's
5
+ all we need for the in-process reference adapter — production adapters
6
+ delegate to their own scheduler.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timedelta
12
+
13
+ _FIELD_RANGES = [
14
+ (0, 59), # minute
15
+ (0, 23), # hour
16
+ (1, 31), # day of month
17
+ (1, 12), # month
18
+ (0, 6), # day of week (0=Mon...6=Sun for simplicity)
19
+ ]
20
+
21
+
22
+ def _parse_field(spec: str, lo: int, hi: int) -> set[int]:
23
+ if spec == "*":
24
+ return set(range(lo, hi + 1))
25
+ if spec.startswith("*/"):
26
+ step = int(spec[2:])
27
+ return set(range(lo, hi + 1, step))
28
+ out: set[int] = set()
29
+ for piece in spec.split(","):
30
+ out.add(int(piece))
31
+ return out
32
+
33
+
34
+ def _matches(expr: str, now: datetime) -> bool:
35
+ parts = expr.split()
36
+ if len(parts) != 5:
37
+ msg = f"cron expr must be 5 fields: {expr!r}"
38
+ raise ValueError(msg)
39
+ minute, hour, dom, month, dow = (
40
+ _parse_field(p, *_FIELD_RANGES[i]) for i, p in enumerate(parts)
41
+ )
42
+ return (
43
+ now.minute in minute
44
+ and now.hour in hour
45
+ and now.day in dom
46
+ and now.month in month
47
+ # weekday(): Mon=0..Sun=6 — matches our field convention.
48
+ and now.weekday() in dow
49
+ )
50
+
51
+
52
+ def next_fire(expr: str, after: datetime) -> float:
53
+ """Seconds until the next minute that matches ``expr`` after ``after``.
54
+
55
+ Scans minute-by-minute up to 31 days ahead — fine for the in-memory
56
+ reference adapter. If no match is found, raises.
57
+ """
58
+ candidate = (after + timedelta(minutes=1)).replace(second=0, microsecond=0)
59
+ end = candidate + timedelta(days=31)
60
+ while candidate < end:
61
+ if _matches(expr, candidate):
62
+ return (candidate - after).total_seconds()
63
+ candidate += timedelta(minutes=1)
64
+ msg = f"no cron match within 31 days for {expr!r}"
65
+ raise ValueError(msg)