py-app-runner 0.5.49.dev0__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 (107) hide show
  1. py_app_runner-0.5.49.dev0/.version +1 -0
  2. py_app_runner-0.5.49.dev0/LICENSE +21 -0
  3. py_app_runner-0.5.49.dev0/MANIFEST.in +1 -0
  4. py_app_runner-0.5.49.dev0/PKG-INFO +134 -0
  5. py_app_runner-0.5.49.dev0/README.md +87 -0
  6. py_app_runner-0.5.49.dev0/pyproject.toml +117 -0
  7. py_app_runner-0.5.49.dev0/setup.cfg +4 -0
  8. py_app_runner-0.5.49.dev0/src/py_app_runner/__init__.py +11 -0
  9. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/__init__.py +29 -0
  10. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/_service.py +91 -0
  11. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/_service_args.py +44 -0
  12. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/audit.py +319 -0
  13. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/commands.py +151 -0
  14. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/diff.py +202 -0
  15. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/errors.py +8 -0
  16. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/event.py +130 -0
  17. py_app_runner-0.5.49.dev0/src/py_app_runner/audit/store.py +134 -0
  18. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/__init__.py +0 -0
  19. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/_service.py +265 -0
  20. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/_service_args.py +24 -0
  21. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/api.py +138 -0
  22. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/encoders/__init__.py +5 -0
  23. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/encoders/base.py +24 -0
  24. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/encoders/json_encoder.py +26 -0
  25. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/encoders/msgpack_encoder.py +58 -0
  26. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/web_app.py +31 -0
  27. py_app_runner-0.5.49.dev0/src/py_app_runner/bridge/websocket.py +313 -0
  28. py_app_runner-0.5.49.dev0/src/py_app_runner/colors.py +73 -0
  29. py_app_runner-0.5.49.dev0/src/py_app_runner/config.py +132 -0
  30. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/__init__.py +14 -0
  31. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/_service.py +75 -0
  32. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/_service_args.py +54 -0
  33. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/commands.py +164 -0
  34. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/envelope.py +144 -0
  35. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/errors.py +8 -0
  36. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/fields.py +300 -0
  37. py_app_runner-0.5.49.dev0/src/py_app_runner/crypto/passwords.py +66 -0
  38. py_app_runner-0.5.49.dev0/src/py_app_runner/db_pools.py +20 -0
  39. py_app_runner-0.5.49.dev0/src/py_app_runner/http_exception.py +31 -0
  40. py_app_runner-0.5.49.dev0/src/py_app_runner/logger_handlers.py +167 -0
  41. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/__init__.py +5 -0
  42. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/_service.py +296 -0
  43. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/_service_args.py +91 -0
  44. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/commands.py +386 -0
  45. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/discovery.py +108 -0
  46. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/states.py +63 -0
  47. py_app_runner-0.5.49.dev0/src/py_app_runner/migrations/tracker.py +141 -0
  48. py_app_runner-0.5.49.dev0/src/py_app_runner/py.typed +0 -0
  49. py_app_runner-0.5.49.dev0/src/py_app_runner/pybridge.py +64 -0
  50. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/__init__.py +25 -0
  51. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/_service.py +231 -0
  52. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/_service_args.py +67 -0
  53. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/commands.py +180 -0
  54. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/driver_pg.py +464 -0
  55. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/driver_redis.py +613 -0
  56. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/handler.py +90 -0
  57. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/interface.py +63 -0
  58. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/job.py +46 -0
  59. py_app_runner-0.5.49.dev0/src/py_app_runner/queue/worker.py +221 -0
  60. py_app_runner-0.5.49.dev0/src/py_app_runner/registry.py +54 -0
  61. py_app_runner-0.5.49.dev0/src/py_app_runner/request_handler/__init__.py +0 -0
  62. py_app_runner-0.5.49.dev0/src/py_app_runner/request_handler/auth_service.py +123 -0
  63. py_app_runner-0.5.49.dev0/src/py_app_runner/request_handler/decorators.py +304 -0
  64. py_app_runner-0.5.49.dev0/src/py_app_runner/request_handler/handlers.py +604 -0
  65. py_app_runner-0.5.49.dev0/src/py_app_runner/request_handler/pagination.py +24 -0
  66. py_app_runner-0.5.49.dev0/src/py_app_runner/return_model.py +78 -0
  67. py_app_runner-0.5.49.dev0/src/py_app_runner/runner.py +182 -0
  68. py_app_runner-0.5.49.dev0/src/py_app_runner/throttle/__init__.py +5 -0
  69. py_app_runner-0.5.49.dev0/src/py_app_runner/throttle/throttle.py +217 -0
  70. py_app_runner-0.5.49.dev0/src/py_app_runner/tick_service.py +308 -0
  71. py_app_runner-0.5.49.dev0/src/py_app_runner/timer.py +289 -0
  72. py_app_runner-0.5.49.dev0/src/py_app_runner/utils.py +346 -0
  73. py_app_runner-0.5.49.dev0/src/py_app_runner/wbcm/__init__.py +0 -0
  74. py_app_runner-0.5.49.dev0/src/py_app_runner/wbcm/device_connections.py +89 -0
  75. py_app_runner-0.5.49.dev0/src/py_app_runner/wbcm/factory.py +113 -0
  76. py_app_runner-0.5.49.dev0/src/py_app_runner/wbcm/wb_connection_manager.py +333 -0
  77. py_app_runner-0.5.49.dev0/src/py_app_runner/wbcm/ws_interface.py +56 -0
  78. py_app_runner-0.5.49.dev0/src/py_app_runner.egg-info/PKG-INFO +134 -0
  79. py_app_runner-0.5.49.dev0/src/py_app_runner.egg-info/SOURCES.txt +105 -0
  80. py_app_runner-0.5.49.dev0/src/py_app_runner.egg-info/dependency_links.txt +1 -0
  81. py_app_runner-0.5.49.dev0/src/py_app_runner.egg-info/requires.txt +19 -0
  82. py_app_runner-0.5.49.dev0/src/py_app_runner.egg-info/top_level.txt +1 -0
  83. py_app_runner-0.5.49.dev0/tests/test_api_keys.py +27 -0
  84. py_app_runner-0.5.49.dev0/tests/test_audit_diff.py +127 -0
  85. py_app_runner-0.5.49.dev0/tests/test_audit_store.py +362 -0
  86. py_app_runner-0.5.49.dev0/tests/test_config.py +63 -0
  87. py_app_runner-0.5.49.dev0/tests/test_crypto_envelope.py +128 -0
  88. py_app_runner-0.5.49.dev0/tests/test_crypto_fields.py +222 -0
  89. py_app_runner-0.5.49.dev0/tests/test_crypto_passwords.py +61 -0
  90. py_app_runner-0.5.49.dev0/tests/test_decorators.py +200 -0
  91. py_app_runner-0.5.49.dev0/tests/test_error_envelope.py +132 -0
  92. py_app_runner-0.5.49.dev0/tests/test_example.py +5 -0
  93. py_app_runner-0.5.49.dev0/tests/test_logger_handlers.py +36 -0
  94. py_app_runner-0.5.49.dev0/tests/test_migrations_commands.py +919 -0
  95. py_app_runner-0.5.49.dev0/tests/test_migrations_discovery.py +166 -0
  96. py_app_runner-0.5.49.dev0/tests/test_migrations_pg_harness.py +35 -0
  97. py_app_runner-0.5.49.dev0/tests/test_migrations_service.py +801 -0
  98. py_app_runner-0.5.49.dev0/tests/test_migrations_states.py +87 -0
  99. py_app_runner-0.5.49.dev0/tests/test_migrations_tracker.py +188 -0
  100. py_app_runner-0.5.49.dev0/tests/test_pybridge.py +76 -0
  101. py_app_runner-0.5.49.dev0/tests/test_queue_contract.py +299 -0
  102. py_app_runner-0.5.49.dev0/tests/test_queue_driver.py +318 -0
  103. py_app_runner-0.5.49.dev0/tests/test_queue_driver_redis.py +250 -0
  104. py_app_runner-0.5.49.dev0/tests/test_queue_worker.py +271 -0
  105. py_app_runner-0.5.49.dev0/tests/test_throttle.py +217 -0
  106. py_app_runner-0.5.49.dev0/tests/test_utils.py +31 -0
  107. py_app_runner-0.5.49.dev0/tests/test_wbcm.py +312 -0
@@ -0,0 +1 @@
1
+ 0.5.49.dev0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Gints Murāns
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ include .version
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: py_app_runner
3
+ Version: 0.5.49.dev0
4
+ Summary: Async service framework: Tornado HTTP/WS bridge, service loader, Redis WS connection manager
5
+ Author-email: Gints Murans <gm@gm.lv>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/4Apps/py_app_runner
8
+ Project-URL: Documentation, https://github.com/4Apps/py_app_runner/blob/master/AGENTS.md
9
+ Project-URL: Code, https://github.com/4Apps/py_app_runner
10
+ Project-URL: Issue Tracker, https://github.com/4Apps/py_app_runner/issues
11
+ Project-URL: Download, https://pypi.org/project/py-app-runner/
12
+ Keywords: async,tornado,websocket,framework,service,redis,postgresql,migrations,queue
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: Implementation :: CPython
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: sentry-sdk
29
+ Requires-Dist: python-dotenv
30
+ Requires-Dist: uvloop
31
+ Requires-Dist: msgspec
32
+ Requires-Dist: msgpack
33
+ Requires-Dist: tornado
34
+ Requires-Dist: database_wrapper[pgsql,redis]==0.3.146
35
+ Requires-Dist: psycopg>=3.2.0
36
+ Requires-Dist: psycopg[pool]>=3.2.0
37
+ Requires-Dist: redis
38
+ Requires-Dist: pyjwt
39
+ Requires-Dist: cryptography>=44
40
+ Requires-Dist: bcrypt
41
+ Provides-Extra: dev
42
+ Requires-Dist: pyrefly; extra == "dev"
43
+ Requires-Dist: ruff; extra == "dev"
44
+ Requires-Dist: pytest; extra == "dev"
45
+ Requires-Dist: pytest-asyncio; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # py_app_runner
49
+
50
+ Async Python service framework: Tornado HTTP/WS bridge, PyBridge service loader, Redis WebSocket connection manager.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install py_app_runner
56
+ ```
57
+
58
+ Every push to `develop` publishes a pre-release, `X.Y.<commit count>.dev0`. pip hides those
59
+ unless you ask for them:
60
+
61
+ ```bash
62
+ pip install --pre py_app_runner # newest, including dev builds
63
+ pip install py_app_runner==0.4.50.dev0 # a specific dev build
64
+ ```
65
+
66
+ ## Development
67
+
68
+ ```bash
69
+ docker-compose up develop
70
+ ```
71
+
72
+ ## Migrations
73
+
74
+ Built-in service that applies tracked SQL files to one or more configured databases. Add
75
+ `migrations` to `SERVICES` to enable it.
76
+
77
+ ```bash
78
+ python3 src/app.py migrations status [--check] [--target NAME]
79
+ python3 src/app.py migrations apply [--dry-run] [--to PREFIX] [--target NAME]
80
+ python3 src/app.py migrations baseline [--to PREFIX] [--yes] [--target NAME]
81
+ python3 src/app.py migrations new <name> [--target NAME]
82
+ python3 src/app.py migrations repair <filename> [--target NAME]
83
+ ```
84
+
85
+ By default there is a single target, `main`, against `config["db"]["main"]`: files live in
86
+ `config["migrations"]["dir"]` (default `data/migrations`) and are tracked in
87
+ `config["migrations"]["table"]` (default `migrations`). **No project needs to change
88
+ anything to keep this working** - an absent `migrations` key, an empty one, and this flat
89
+ shape all resolve to that same single target.
90
+
91
+ To migrate more than one database, opt in with `config["migrations"]["targets"]`:
92
+
93
+ ```python
94
+ "migrations": {
95
+ "targets": {
96
+ "main": {"db": "main", "dir": "data/migrations", "table": "migrations"},
97
+ "gis": {"db": "gis", "dir": "data/migrations_gis", "table": "gis_migrations"},
98
+ }
99
+ }
100
+ ```
101
+
102
+ Each target's `db` names a key under `config["db"]` and defaults to the target's own name;
103
+ `dir` and `table` default as above. `status` and `apply` with no `--target` run every
104
+ target in declared order (`apply` stops at the first one that fails; `status --check`
105
+ reports each target rather than stopping at the first with pending work, and exits 1 if any
106
+ is pending or blocked). `new`, `repair` and `baseline` require `--target` once more than one
107
+ target is configured, and an unknown `--target` exits 1 - both name the configured targets.
108
+ Output gets a `[name] ` prefix only when more than one target is processed, so single-target
109
+ output is unchanged.
110
+
111
+ Targets may share a database, but not a database *and* a tracking table - each would then
112
+ report the other's migrations as missing, so that config is refused up front, naming both
113
+ targets. `table` defaults to `migrations` for every target, so two targets on one database
114
+ need an explicit `table` on at least one of them.
115
+
116
+ - Migrations no longer need to be idempotent - each file runs in its own transaction with
117
+ its tracking row written inside it, so what already ran is always known.
118
+ - `-- migrations:no-transaction` on line 1 runs a file outside a transaction (for
119
+ `CREATE INDEX CONCURRENTLY` and friends). Such a file must contain **exactly one
120
+ statement**: Postgres wraps a multi-statement send in an implicit transaction, which would
121
+ defeat the directive, so `apply` refuses it up front. A no-transaction file that fails
122
+ cannot roll back and is not recorded - `apply` says so and tells you to inspect the
123
+ database before re-running.
124
+ - Editing an applied file is detected as drift, and a tracked file that has since been
125
+ deleted shows as missing; both block `apply` until resolved. `repair` fixes drift only;
126
+ a missing file is fixed by restoring it, or by deleting its tracking row by hand
127
+ (`apply` prints the exact `DELETE` when it blocks).
128
+ - Files must not contain psql meta-commands (`\restrict` / `\unrestrict`, as emitted by
129
+ `pg_dump`) - psycopg cannot execute them, and `apply` refuses such a file up front.
130
+ - `baseline` adopts an existing database into the system: it writes tracking rows without
131
+ executing anything.
132
+ - Every subcommand exits non-zero on failure, including a misconfigured `targets` block.
133
+ `status --check` exits 1 if anything is pending, drifted or missing, so a deploy script
134
+ can halt before restarting services against a half-migrated database.
@@ -0,0 +1,87 @@
1
+ # py_app_runner
2
+
3
+ Async Python service framework: Tornado HTTP/WS bridge, PyBridge service loader, Redis WebSocket connection manager.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install py_app_runner
9
+ ```
10
+
11
+ Every push to `develop` publishes a pre-release, `X.Y.<commit count>.dev0`. pip hides those
12
+ unless you ask for them:
13
+
14
+ ```bash
15
+ pip install --pre py_app_runner # newest, including dev builds
16
+ pip install py_app_runner==0.4.50.dev0 # a specific dev build
17
+ ```
18
+
19
+ ## Development
20
+
21
+ ```bash
22
+ docker-compose up develop
23
+ ```
24
+
25
+ ## Migrations
26
+
27
+ Built-in service that applies tracked SQL files to one or more configured databases. Add
28
+ `migrations` to `SERVICES` to enable it.
29
+
30
+ ```bash
31
+ python3 src/app.py migrations status [--check] [--target NAME]
32
+ python3 src/app.py migrations apply [--dry-run] [--to PREFIX] [--target NAME]
33
+ python3 src/app.py migrations baseline [--to PREFIX] [--yes] [--target NAME]
34
+ python3 src/app.py migrations new <name> [--target NAME]
35
+ python3 src/app.py migrations repair <filename> [--target NAME]
36
+ ```
37
+
38
+ By default there is a single target, `main`, against `config["db"]["main"]`: files live in
39
+ `config["migrations"]["dir"]` (default `data/migrations`) and are tracked in
40
+ `config["migrations"]["table"]` (default `migrations`). **No project needs to change
41
+ anything to keep this working** - an absent `migrations` key, an empty one, and this flat
42
+ shape all resolve to that same single target.
43
+
44
+ To migrate more than one database, opt in with `config["migrations"]["targets"]`:
45
+
46
+ ```python
47
+ "migrations": {
48
+ "targets": {
49
+ "main": {"db": "main", "dir": "data/migrations", "table": "migrations"},
50
+ "gis": {"db": "gis", "dir": "data/migrations_gis", "table": "gis_migrations"},
51
+ }
52
+ }
53
+ ```
54
+
55
+ Each target's `db` names a key under `config["db"]` and defaults to the target's own name;
56
+ `dir` and `table` default as above. `status` and `apply` with no `--target` run every
57
+ target in declared order (`apply` stops at the first one that fails; `status --check`
58
+ reports each target rather than stopping at the first with pending work, and exits 1 if any
59
+ is pending or blocked). `new`, `repair` and `baseline` require `--target` once more than one
60
+ target is configured, and an unknown `--target` exits 1 - both name the configured targets.
61
+ Output gets a `[name] ` prefix only when more than one target is processed, so single-target
62
+ output is unchanged.
63
+
64
+ Targets may share a database, but not a database *and* a tracking table - each would then
65
+ report the other's migrations as missing, so that config is refused up front, naming both
66
+ targets. `table` defaults to `migrations` for every target, so two targets on one database
67
+ need an explicit `table` on at least one of them.
68
+
69
+ - Migrations no longer need to be idempotent - each file runs in its own transaction with
70
+ its tracking row written inside it, so what already ran is always known.
71
+ - `-- migrations:no-transaction` on line 1 runs a file outside a transaction (for
72
+ `CREATE INDEX CONCURRENTLY` and friends). Such a file must contain **exactly one
73
+ statement**: Postgres wraps a multi-statement send in an implicit transaction, which would
74
+ defeat the directive, so `apply` refuses it up front. A no-transaction file that fails
75
+ cannot roll back and is not recorded - `apply` says so and tells you to inspect the
76
+ database before re-running.
77
+ - Editing an applied file is detected as drift, and a tracked file that has since been
78
+ deleted shows as missing; both block `apply` until resolved. `repair` fixes drift only;
79
+ a missing file is fixed by restoring it, or by deleting its tracking row by hand
80
+ (`apply` prints the exact `DELETE` when it blocks).
81
+ - Files must not contain psql meta-commands (`\restrict` / `\unrestrict`, as emitted by
82
+ `pg_dump`) - psycopg cannot execute them, and `apply` refuses such a file up front.
83
+ - `baseline` adopts an existing database into the system: it writes tracking rows without
84
+ executing anything.
85
+ - Every subcommand exits non-zero on failure, including a misconfigured `targets` block.
86
+ `status --check` exits 1 if anything is pending, drifted or missing, so a deploy script
87
+ can halt before restarting services against a half-migrated database.
@@ -0,0 +1,117 @@
1
+ [build-system]
2
+ # PEP 639 license expressions (the "license"/"license-files" keys below) need 77+
3
+ requires = ["setuptools >= 77.0.0"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "py_app_runner"
8
+ dynamic = ["version"]
9
+ description = "Async service framework: Tornado HTTP/WS bridge, service loader, Redis WS connection manager"
10
+ readme = "README.md"
11
+ requires-python = ">=3.11"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [{ name = "Gints Murans", email = "gm@gm.lv" }]
15
+ keywords = [
16
+ "async",
17
+ "tornado",
18
+ "websocket",
19
+ "framework",
20
+ "service",
21
+ "redis",
22
+ "postgresql",
23
+ "migrations",
24
+ "queue",
25
+ ]
26
+ # No "License ::" classifier: it is deprecated alongside Metadata 2.4, and setuptools
27
+ # refuses one next to the license expression above
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Framework :: AsyncIO",
31
+ "Intended Audience :: Developers",
32
+ # uvloop is a hard dependency and does not build on Windows
33
+ "Operating System :: POSIX",
34
+ "Programming Language :: Python :: 3",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Programming Language :: Python :: Implementation :: CPython",
39
+ "Topic :: Internet :: WWW/HTTP",
40
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
41
+ "Typing :: Typed",
42
+ ]
43
+
44
+ dependencies = [
45
+ "sentry-sdk",
46
+ "python-dotenv",
47
+ "uvloop",
48
+ "msgspec",
49
+ "msgpack",
50
+ "tornado",
51
+ # The extras pull the base package in, so one pin rather than three that must be kept
52
+ # in lockstep. 0.3.146 is the first MIT release - 0.2.38 and earlier are GPL-3.0.
53
+ "database_wrapper[pgsql,redis] == 0.3.146",
54
+ # Used directly by py_app_runner.migrations, not only through database_wrapper, so the
55
+ # floor is declared here rather than inherited. Matches database_wrapper_pgsql 0.3.146's
56
+ # own psycopg[binary]/psycopg[pool] >= 3.2.0.
57
+ "psycopg >= 3.2.0",
58
+ "psycopg[pool] >= 3.2.0",
59
+ "redis",
60
+ "pyjwt",
61
+ # Used directly by py_app_runner.crypto for AES-256-GCM. The floor is 44 because that
62
+ # is where XChaCha20Poly1305 appears, which is the obvious alternative if AES-GCM's
63
+ # 96-bit nonce ever becomes uncomfortable for a high-volume column.
64
+ "cryptography >= 44",
65
+ "bcrypt",
66
+ ]
67
+
68
+ [project.optional-dependencies]
69
+ dev = [
70
+ "pyrefly",
71
+ "ruff",
72
+ "pytest",
73
+ "pytest-asyncio",
74
+ ]
75
+
76
+ [project.urls]
77
+ Homepage = "https://github.com/4Apps/py_app_runner"
78
+ Documentation = "https://github.com/4Apps/py_app_runner/blob/master/AGENTS.md"
79
+ Code = "https://github.com/4Apps/py_app_runner"
80
+ "Issue Tracker" = "https://github.com/4Apps/py_app_runner/issues"
81
+ Download = "https://pypi.org/project/py-app-runner/"
82
+
83
+ [tool.setuptools.packages.find]
84
+ where = ["src"]
85
+
86
+ # Without the marker file in the wheel, a downstream type checker ignores every
87
+ # annotation in this package (PEP 561)
88
+ [tool.setuptools.package-data]
89
+ "py_app_runner" = ["py.typed"]
90
+
91
+ # Manual major.minor in .version; CI appends the git commit count as the patch part.
92
+ # A local build therefore reports a bare "0.4" - scripts/version.bash prints what a
93
+ # release of this commit would actually publish as.
94
+ [tool.setuptools.dynamic]
95
+ version = { file = ".version" }
96
+
97
+ [tool.pytest.ini_options]
98
+ asyncio_mode = "auto"
99
+ testpaths = ["tests"]
100
+ asyncio_default_fixture_loop_scope = "function"
101
+
102
+ [tool.ruff]
103
+ line-length = 120
104
+ target-version = "py311"
105
+ src = ["src"]
106
+
107
+ [tool.ruff.lint]
108
+ select = ["E", "F", "I", "B", "UP"]
109
+ fixable = ["ALL"]
110
+
111
+ [tool.ruff.format]
112
+ quote-style = "double"
113
+ indent-style = "space"
114
+
115
+ # For some reason this needs to be here to show errors in VSCode
116
+ [tool.pyrefly]
117
+ site-package-path = ["/srv/venv/lib64/python3.13/site-packages"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from py_app_runner.registry import AppRegistry
4
+
5
+ __all__ = ["AppRegistry"]
6
+
7
+ try:
8
+ __version__ = version("py_app_runner")
9
+ except PackageNotFoundError:
10
+ # Running straight from a source tree that was never installed
11
+ __version__ = "0.0.dev0"
@@ -0,0 +1,29 @@
1
+ """An append-only trail of who changed what, written explicitly at the call site."""
2
+
3
+ from py_app_runner.audit.audit import Audit
4
+ from py_app_runner.audit.errors import AuditError
5
+ from py_app_runner.audit.event import (
6
+ CREATED,
7
+ DELETED,
8
+ UPDATED,
9
+ Actor,
10
+ AuditEvent,
11
+ RequestContext,
12
+ current_context,
13
+ new_request_id,
14
+ request_context,
15
+ )
16
+
17
+ __all__ = [
18
+ "CREATED",
19
+ "DELETED",
20
+ "UPDATED",
21
+ "Actor",
22
+ "Audit",
23
+ "AuditError",
24
+ "AuditEvent",
25
+ "RequestContext",
26
+ "current_context",
27
+ "new_request_id",
28
+ "request_context",
29
+ ]
@@ -0,0 +1,91 @@
1
+ import asyncio
2
+ import logging
3
+ import pathlib
4
+ from argparse import Namespace
5
+ from datetime import UTC, datetime
6
+
7
+ import psycopg
8
+
9
+ from py_app_runner.audit.commands import Out, cmd_install, cmd_prune
10
+ from py_app_runner.audit.errors import AuditError
11
+ from py_app_runner.migrations._service import connect_kwargs, resolve_targets
12
+ from py_app_runner.pybridge import PyBridge
13
+ from py_app_runner.registry import AppRegistry
14
+
15
+ _DEFAULT_DB = "main"
16
+ _DEFAULT_TABLE = "audit_log"
17
+
18
+
19
+ def _migrations_dir(args: Namespace, config: dict) -> pathlib.Path:
20
+ """Where `install` writes.
21
+
22
+ The file it produces is a migration and belongs wherever the rest of them are, so the
23
+ default comes from the migrations config rather than from an audit key of its own.
24
+ """
25
+
26
+ raw = getattr(args, "dir", None)
27
+ if raw:
28
+ directory = pathlib.Path(raw)
29
+ if directory.is_absolute():
30
+ return directory
31
+
32
+ return pathlib.Path(config.get("current_path") or ".") / directory
33
+
34
+ targets = resolve_targets(config)
35
+ return next(iter(targets.values())).directory
36
+
37
+
38
+ async def init_service(args: Namespace, _pybridge: PyBridge, logger: logging.Logger) -> None:
39
+ out: Out = print
40
+
41
+ # Same reasoning as migrations: runner.py catches Exception around init_service and
42
+ # returns normally, which exits 0. A prune that could not reach the database must not
43
+ # report success to a retention job.
44
+ code = 1
45
+ try:
46
+ config = AppRegistry.config()
47
+ settings = config.get("audit") or {}
48
+ table = getattr(args, "table", None) or settings.get("table") or _DEFAULT_TABLE
49
+
50
+ if args.step == "install":
51
+ raise SystemExit(cmd_install(_migrations_dir(args, config), table, datetime.now(UTC), out))
52
+
53
+ db_name = getattr(args, "db", None) or settings.get("db") or _DEFAULT_DB
54
+ db_config = config.get("db") or {}
55
+ if db_name not in db_config:
56
+ out(
57
+ f'error: no database {db_name!r} in config["db"]; '
58
+ f"configured are: {', '.join(sorted(db_config)) or 'none'}."
59
+ )
60
+ raise SystemExit(2)
61
+
62
+ if args.step == "prune":
63
+ async with await psycopg.AsyncConnection.connect(**connect_kwargs(db_config[db_name])) as conn:
64
+ code = await cmd_prune(
65
+ conn,
66
+ table,
67
+ args.before,
68
+ args.batch,
69
+ getattr(args, "dry_run", False),
70
+ out,
71
+ )
72
+ else:
73
+ out(f"error: unknown audit command {args.step!r}")
74
+ code = 1
75
+
76
+ except AuditError as e:
77
+ # Configuration and refusal messages already say what to do; a stack trace above
78
+ # them would bury it.
79
+ out(f"error: {e}")
80
+ raise SystemExit(1) from None
81
+ except (KeyboardInterrupt, asyncio.CancelledError):
82
+ # Above `except Exception` because both derive from BaseException. An interrupted
83
+ # prune has deleted some batches and not others - safe to resume, but it must not
84
+ # be reported as complete.
85
+ logger.error("audit: interrupted; the prune is partially done")
86
+ raise SystemExit(1) from None
87
+ except Exception:
88
+ logger.exception("audit: unhandled failure")
89
+ raise SystemExit(1) from None
90
+
91
+ raise SystemExit(code)
@@ -0,0 +1,44 @@
1
+ """CLI subparsers for the built-in audit service.
2
+
3
+ python3 src/app.py audit install [--dir PATH] [--table NAME]
4
+ python3 src/app.py audit prune --before YYYY-MM-DD [--batch N] [--dry-run] [--db NAME]
5
+
6
+ `--dry-run` shadows a real top-level flag on runner.py's parser and needs
7
+ `default=SUPPRESS`, the same collision migrations' `apply` has.
8
+ """
9
+
10
+ import logging
11
+ from argparse import SUPPRESS, ArgumentParser, _SubParsersAction # type: ignore
12
+
13
+ from py_app_runner.pybridge import PyBridge
14
+
15
+
16
+ def reg_subparsers(
17
+ subparsers: "_SubParsersAction[ArgumentParser]",
18
+ _pybridge: PyBridge,
19
+ _base_logger: logging.Logger,
20
+ ) -> None:
21
+ """Command line subparsers"""
22
+
23
+ parser = subparsers.add_parser(
24
+ "audit",
25
+ description="Install the audit trail schema and prune old rows",
26
+ help="Audit trail",
27
+ )
28
+ group = parser.add_subparsers(title="command", dest="step", required=True)
29
+
30
+ install_parser = group.add_parser("install", help="Write the audit schema into the migrations directory")
31
+ install_parser.add_argument("--dir", default=None, help="Migrations directory to write into")
32
+ install_parser.add_argument("--table", default=None, help="Audit table (default: from config)")
33
+
34
+ prune_parser = group.add_parser("prune", help="Delete trail rows older than a date")
35
+ prune_parser.add_argument("--before", required=True, help="Delete rows older than this, YYYY-MM-DD")
36
+ prune_parser.add_argument("--batch", type=int, default=10000, help="Rows per statement (default: 10000)")
37
+ prune_parser.add_argument("--table", default=None, help="Audit table (default: from config)")
38
+ prune_parser.add_argument("--db", default=None, help='Entry of config["db"] to prune in (default: main)')
39
+ prune_parser.add_argument(
40
+ "--dry-run",
41
+ action="store_true",
42
+ default=SUPPRESS,
43
+ help="Count what would go, delete nothing",
44
+ )