tigrbl 0.3.29.dev2__tar.gz → 0.4.0.dev2__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.
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/PKG-INFO +10 -1
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/README.md +4 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/pyproject.toml +10 -2
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/__init__.py +58 -0
- tigrbl-0.4.0.dev2/tigrbl/canonical_json.py +7 -0
- tigrbl-0.4.0.dev2/tigrbl/session/__init__.py +8 -0
- tigrbl-0.4.0.dev2/tigrbl/session/base.py +7 -0
- tigrbl-0.4.0.dev2/tigrbl/session/spec.py +7 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/diagnostics/__init__.py +10 -0
- tigrbl-0.4.0.dev2/tigrbl/system/diagnostics/healthz.py +285 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/types/__init__.py +0 -16
- tigrbl-0.3.29.dev2/tigrbl/system/diagnostics/healthz.py +0 -69
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/LICENSE +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/__main__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/cli.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/config/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/config/constants.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/config/defaults.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/ddl/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/allow_anon.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/engine.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/hook.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/middlewares.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/op.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/response.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/rest.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/schema.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/decorators/session.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/bind.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/builders.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/capabilities.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/collect.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/plugins.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/registry.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/engine/resolver.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/app.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/column.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/engine.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/hook.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/op.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/responses.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/router.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/schema.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/factories/table.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/hook/exceptions.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/hook/types.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/middlewares/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/middlewares/compose.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/op/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/op/canonical.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/op/collect.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/op/types.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/orm/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/requests.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/rest/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/build_schema.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/cache.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/extras.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/helpers.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/list_params.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/builder/strip_parent_fields.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/schema/utils.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/security/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/security/dependencies.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/app.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/column.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/engine.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/hook.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/op.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/responses.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/rest.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/router.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/schema.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/shortcuts/table.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/specs.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/diagnostics/hookz.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/diagnostics/kernelz.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/diagnostics/methodz.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/diagnostics/router.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/diagnostics/utils.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/lens.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/openapi/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/openapi/helpers.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/openapi/metadata.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/openapi/mount.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/openapi/schema.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/openrpc.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/docs/swagger.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/favicon/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/favicon/assets/favicon.svg +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/system/uvicorn.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/transport/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/transport/jsonrpc/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/transport/jsonrpc/helpers.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/transport/jsonrpc/models.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/transport/rest/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/transport/rest/aggregator.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/utils/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/utils/schema.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/vendor/__init__.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/vendor/jinja.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/vendor/pydantic.py +0 -0
- {tigrbl-0.3.29.dev2 → tigrbl-0.4.0.dev2}/tigrbl/vendor/sqlalchemy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrbl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
4
|
Summary: A modern pure ASGI/WSGI Python framework for building schema-first REST and JSON-RPC APIs with SQLAlchemy models, typed validation, lifecycle hooks, and engine extension support.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -40,6 +40,11 @@ Requires-Dist: tigrbl-orm
|
|
|
40
40
|
Requires-Dist: tigrbl-runtime
|
|
41
41
|
Requires-Dist: tigrbl-tests ; extra == "tests"
|
|
42
42
|
Requires-Dist: uvicorn
|
|
43
|
+
Project-URL: Discord, https://discord.gg/K4YTAPapjR
|
|
44
|
+
Project-URL: Homepage, https://github.com/tigrbl/tigrbl
|
|
45
|
+
Project-URL: Issues, https://github.com/tigrbl/tigrbl/issues
|
|
46
|
+
Project-URL: Organization, https://github.com/tigrbl
|
|
47
|
+
Project-URL: Repository, https://github.com/tigrbl/tigrbl
|
|
43
48
|
Description-Content-Type: text/markdown
|
|
44
49
|
|
|
45
50
|
# tigrbl
|
|
@@ -60,6 +65,10 @@ It is not the authoritative location for repository governance, current target s
|
|
|
60
65
|
|
|
61
66
|
## Package identity
|
|
62
67
|
|
|
68
|
+
- canonical repository: `https://github.com/tigrbl/tigrbl`
|
|
69
|
+
- organization: `https://github.com/tigrbl`
|
|
70
|
+
- social: `https://discord.gg/K4YTAPapjR`
|
|
71
|
+
- package path: `https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl`
|
|
63
72
|
- workspace path: `pkgs/core/tigrbl`
|
|
64
73
|
- workspace class: core Python package
|
|
65
74
|
- implementation layout: `tigrbl/`
|
|
@@ -16,6 +16,10 @@ It is not the authoritative location for repository governance, current target s
|
|
|
16
16
|
|
|
17
17
|
## Package identity
|
|
18
18
|
|
|
19
|
+
- canonical repository: `https://github.com/tigrbl/tigrbl`
|
|
20
|
+
- organization: `https://github.com/tigrbl`
|
|
21
|
+
- social: `https://discord.gg/K4YTAPapjR`
|
|
22
|
+
- package path: `https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl`
|
|
19
23
|
- workspace path: `pkgs/core/tigrbl`
|
|
20
24
|
- workspace class: core Python package
|
|
21
25
|
- implementation layout: `tigrbl/`
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tigrbl"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0.dev2"
|
|
4
4
|
description = "A modern pure ASGI/WSGI Python framework for building schema-first REST and JSON-RPC APIs with SQLAlchemy models, typed validation, lifecycle hooks, and engine extension support."
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
readme = "README.md"
|
|
7
|
-
repository = "http://github.com/swarmauri/swarmauri-sdk"
|
|
8
7
|
requires-python = ">=3.10,<3.14"
|
|
9
8
|
classifiers = [
|
|
10
9
|
"Development Status :: 3 - Alpha",
|
|
@@ -17,6 +16,7 @@ classifiers = [
|
|
|
17
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
18
17
|
]
|
|
19
18
|
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
19
|
+
|
|
20
20
|
dependencies = [
|
|
21
21
|
"tigrbl-core",
|
|
22
22
|
"tigrbl-base",
|
|
@@ -36,6 +36,14 @@ dependencies = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Organization = "https://github.com/tigrbl"
|
|
42
|
+
Discord = "https://discord.gg/K4YTAPapjR"
|
|
43
|
+
Homepage = "https://github.com/tigrbl/tigrbl"
|
|
44
|
+
Repository = "https://github.com/tigrbl/tigrbl"
|
|
45
|
+
Issues = "https://github.com/tigrbl/tigrbl/issues"
|
|
46
|
+
|
|
39
47
|
[project.scripts]
|
|
40
48
|
tigrbl = "tigrbl.cli:console_main"
|
|
41
49
|
|
|
@@ -101,6 +101,7 @@ from tigrbl_concrete._concrete import ( # noqa: E402
|
|
|
101
101
|
PlainTextResponse,
|
|
102
102
|
RedirectResponse,
|
|
103
103
|
Request,
|
|
104
|
+
TransportResponse,
|
|
104
105
|
UploadedFile,
|
|
105
106
|
WebSocket,
|
|
106
107
|
Response,
|
|
@@ -148,6 +149,8 @@ from tigrbl.ddl import bootstrap_dbschema, ensure_schemas, register_sqlite_attac
|
|
|
148
149
|
|
|
149
150
|
from tigrbl_base._base import ( # noqa: E402
|
|
150
151
|
AppBase,
|
|
152
|
+
EngineBase,
|
|
153
|
+
EngineProviderBase,
|
|
151
154
|
ForeignKeyBase,
|
|
152
155
|
HookBase,
|
|
153
156
|
RouterBase,
|
|
@@ -161,6 +164,7 @@ from tigrbl_core._spec import ( # noqa: E402
|
|
|
161
164
|
BindingSpec,
|
|
162
165
|
ColumnSpec,
|
|
163
166
|
EngineSpec,
|
|
167
|
+
EngineRegistry,
|
|
164
168
|
Exchange,
|
|
165
169
|
FieldSpec,
|
|
166
170
|
Framing,
|
|
@@ -182,12 +186,18 @@ from tigrbl_core._spec import ( # noqa: E402
|
|
|
182
186
|
SchemaRef,
|
|
183
187
|
SchemaSpec,
|
|
184
188
|
SessionSpec,
|
|
189
|
+
readonly,
|
|
190
|
+
session_spec,
|
|
185
191
|
StorageSpec,
|
|
186
192
|
StorageTransformSpec,
|
|
193
|
+
StorageTypeRef,
|
|
187
194
|
TableRegistrySpec,
|
|
188
195
|
TableSpec,
|
|
189
196
|
TargetOp,
|
|
190
197
|
TemplateSpec,
|
|
198
|
+
tx_read_committed,
|
|
199
|
+
tx_repeatable_read,
|
|
200
|
+
tx_serializable,
|
|
191
201
|
TxScope,
|
|
192
202
|
WebTransportBindingSpec,
|
|
193
203
|
WsBindingSpec,
|
|
@@ -246,12 +256,47 @@ def include_tables(*args, **kwargs):
|
|
|
246
256
|
)
|
|
247
257
|
|
|
248
258
|
|
|
259
|
+
def include_model(*args, **kwargs):
|
|
260
|
+
if len(args) == 1 and isinstance(args[0], type) and not kwargs:
|
|
261
|
+
return args[0], None
|
|
262
|
+
return import_module("tigrbl_concrete._mapping.router.include").include_model(
|
|
263
|
+
*args, **kwargs
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def include_table(*args, **kwargs):
|
|
268
|
+
return import_module("tigrbl_concrete._mapping.router.include").include_table(
|
|
269
|
+
*args, **kwargs
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def include_models(*args, **kwargs):
|
|
274
|
+
return import_module("tigrbl_concrete._mapping.router.include").include_models(
|
|
275
|
+
*args, **kwargs
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
249
279
|
async def rpc_call(*args, **kwargs):
|
|
250
280
|
return await import_module("tigrbl_concrete._mapping.router.rpc").rpc_call(
|
|
251
281
|
*args, **kwargs
|
|
252
282
|
)
|
|
253
283
|
|
|
254
284
|
|
|
285
|
+
from tigrbl_core._spec.session_spec import ( # noqa: E402
|
|
286
|
+
readonly as _readonly_fn,
|
|
287
|
+
session_spec as _session_spec_fn,
|
|
288
|
+
tx_read_committed as _tx_read_committed_fn,
|
|
289
|
+
tx_repeatable_read as _tx_repeatable_read_fn,
|
|
290
|
+
tx_serializable as _tx_serializable_fn,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
readonly = _readonly_fn
|
|
294
|
+
session_spec = _session_spec_fn
|
|
295
|
+
tx_read_committed = _tx_read_committed_fn
|
|
296
|
+
tx_repeatable_read = _tx_repeatable_read_fn
|
|
297
|
+
tx_serializable = _tx_serializable_fn
|
|
298
|
+
|
|
299
|
+
|
|
255
300
|
__all__ = [
|
|
256
301
|
"specs",
|
|
257
302
|
"base",
|
|
@@ -324,6 +369,9 @@ __all__ = [
|
|
|
324
369
|
"build_handlers",
|
|
325
370
|
"register_rpc",
|
|
326
371
|
"build_rest",
|
|
372
|
+
"include_model",
|
|
373
|
+
"include_table",
|
|
374
|
+
"include_models",
|
|
327
375
|
"include_tables",
|
|
328
376
|
"rpc_call",
|
|
329
377
|
"_invoke",
|
|
@@ -339,6 +387,7 @@ __all__ = [
|
|
|
339
387
|
"UploadedFile",
|
|
340
388
|
"WebSocket",
|
|
341
389
|
"Response",
|
|
390
|
+
"TransportResponse",
|
|
342
391
|
"JSONResponse",
|
|
343
392
|
"BackgroundTask",
|
|
344
393
|
"resolver",
|
|
@@ -355,11 +404,20 @@ __all__ = [
|
|
|
355
404
|
"StorageTransform",
|
|
356
405
|
"Middleware",
|
|
357
406
|
"StorageTransformSpec",
|
|
407
|
+
"StorageTypeRef",
|
|
358
408
|
"ForeignKeySpec",
|
|
359
409
|
"RequestSpec",
|
|
360
410
|
"SchemaSpec",
|
|
361
411
|
"SessionSpec",
|
|
412
|
+
"session_spec",
|
|
413
|
+
"tx_read_committed",
|
|
414
|
+
"tx_repeatable_read",
|
|
415
|
+
"tx_serializable",
|
|
416
|
+
"readonly",
|
|
362
417
|
"EngineSpec",
|
|
418
|
+
"EngineRegistry",
|
|
419
|
+
"EngineBase",
|
|
420
|
+
"EngineProviderBase",
|
|
363
421
|
"TemplateSpec",
|
|
364
422
|
"ForeignKeyBase",
|
|
365
423
|
"HookBase",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from ...runtime.kernel import _default_kernel, build_phase_chains
|
|
2
2
|
from .router import mount_diagnostics
|
|
3
|
+
from .healthz import build_healthz_endpoint, build_healthz_html, mount_healthz_uix
|
|
4
|
+
from .methodz import build_methodz_endpoint
|
|
5
|
+
from .hookz import build_hookz_endpoint
|
|
6
|
+
from .kernelz import build_kernelz_endpoint
|
|
3
7
|
from .methodz import build_methodz_endpoint as _build_methodz_endpoint
|
|
4
8
|
from .hookz import build_hookz_endpoint as _build_hookz_endpoint
|
|
5
9
|
from .kernelz import build_kernelz_endpoint as _build_kernelz_endpoint
|
|
@@ -13,6 +17,12 @@ from .utils import (
|
|
|
13
17
|
|
|
14
18
|
__all__ = [
|
|
15
19
|
"mount_diagnostics",
|
|
20
|
+
"build_healthz_endpoint",
|
|
21
|
+
"build_healthz_html",
|
|
22
|
+
"mount_healthz_uix",
|
|
23
|
+
"build_methodz_endpoint",
|
|
24
|
+
"build_hookz_endpoint",
|
|
25
|
+
"build_kernelz_endpoint",
|
|
16
26
|
"_build_methodz_endpoint",
|
|
17
27
|
"_build_hookz_endpoint",
|
|
18
28
|
"_build_kernelz_endpoint",
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from ... import Depends, Request
|
|
9
|
+
from ..._concrete import JSONResponse
|
|
10
|
+
from ..._concrete._response import Response
|
|
11
|
+
from .utils import maybe_execute
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_NO_DB_WARNING = "db-not-configured"
|
|
16
|
+
_UNAVAILABLE_WARNING = "db-unavailable"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_db(candidate: Any) -> Any:
|
|
20
|
+
"""Resolve a DB-like object from either a DB handle or a Request object."""
|
|
21
|
+
if hasattr(candidate, "execute"):
|
|
22
|
+
return candidate
|
|
23
|
+
|
|
24
|
+
state = getattr(candidate, "state", None)
|
|
25
|
+
db = getattr(state, "db", None)
|
|
26
|
+
if db is not None:
|
|
27
|
+
return db
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _has_execute(candidate: Any) -> bool:
|
|
32
|
+
return callable(getattr(candidate, "execute", None))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _query_value(request: Any, name: str, default: str | None = None) -> str | None:
|
|
36
|
+
getter = getattr(request, "query_param", None)
|
|
37
|
+
if callable(getter):
|
|
38
|
+
return getter(name, default)
|
|
39
|
+
query = getattr(request, "query", None) or {}
|
|
40
|
+
values = query.get(name)
|
|
41
|
+
if isinstance(values, (list, tuple)) and values:
|
|
42
|
+
return str(values[0])
|
|
43
|
+
if values is not None:
|
|
44
|
+
return str(values)
|
|
45
|
+
return default
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _query_flag(request: Any, name: str) -> bool:
|
|
49
|
+
value = _query_value(request, name)
|
|
50
|
+
if value is None:
|
|
51
|
+
return False
|
|
52
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on", "warn"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _wants_all_dbs(request: Any) -> bool:
|
|
56
|
+
value = _query_value(request, "dbs") or _query_value(request, "detail")
|
|
57
|
+
return str(value or "").strip().lower() in {"all", "full", "verbose"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _iter_db_entries(candidate: Any) -> list[tuple[str, Any]]:
|
|
61
|
+
if candidate is None:
|
|
62
|
+
return []
|
|
63
|
+
if _has_execute(candidate):
|
|
64
|
+
return [("default", candidate)]
|
|
65
|
+
if isinstance(candidate, Mapping):
|
|
66
|
+
return [
|
|
67
|
+
(str(name), db)
|
|
68
|
+
for name, db in candidate.items()
|
|
69
|
+
if _has_execute(db)
|
|
70
|
+
]
|
|
71
|
+
if isinstance(candidate, Iterable) and not isinstance(
|
|
72
|
+
candidate, (str, bytes, bytearray)
|
|
73
|
+
):
|
|
74
|
+
return [
|
|
75
|
+
(str(index), db)
|
|
76
|
+
for index, db in enumerate(candidate)
|
|
77
|
+
if _has_execute(db)
|
|
78
|
+
]
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolve_db_entries(candidate: Any, *sources: Any) -> list[tuple[str, Any]]:
|
|
83
|
+
entries = _iter_db_entries(candidate)
|
|
84
|
+
if entries:
|
|
85
|
+
return entries
|
|
86
|
+
|
|
87
|
+
state = getattr(candidate, "state", None)
|
|
88
|
+
app = getattr(candidate, "app", None)
|
|
89
|
+
for source in (state, app, *sources):
|
|
90
|
+
if source is None:
|
|
91
|
+
continue
|
|
92
|
+
for attr in ("dbs", "databases", "engines", "db"):
|
|
93
|
+
entries = _iter_db_entries(getattr(source, attr, None))
|
|
94
|
+
if entries:
|
|
95
|
+
return entries
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def _check_db(name: str, db: Any) -> dict[str, Any]:
|
|
100
|
+
try:
|
|
101
|
+
await maybe_execute(db, "SELECT 1")
|
|
102
|
+
return {"name": name, "ok": True}
|
|
103
|
+
except Exception as exc: # pragma: no cover - covered by fixture fakes
|
|
104
|
+
logger.warning("/healthz degraded for %s: %s", name, exc)
|
|
105
|
+
return {
|
|
106
|
+
"name": name,
|
|
107
|
+
"ok": False,
|
|
108
|
+
"warning": _UNAVAILABLE_WARNING,
|
|
109
|
+
"error": str(exc),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _multi_db_payload(results: list[dict[str, Any]], *, include_all: bool) -> dict[str, Any]:
|
|
114
|
+
total = len(results)
|
|
115
|
+
healthy = sum(1 for item in results if item["ok"])
|
|
116
|
+
payload: dict[str, Any] = {
|
|
117
|
+
"ok": healthy == total,
|
|
118
|
+
"dbs": {
|
|
119
|
+
"ok": healthy,
|
|
120
|
+
"total": total,
|
|
121
|
+
"failed": total - healthy,
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
if healthy != total:
|
|
125
|
+
payload["warning"] = _UNAVAILABLE_WARNING
|
|
126
|
+
if include_all:
|
|
127
|
+
payload["databases"] = results
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_healthz_endpoint(
|
|
132
|
+
dep: Optional[Callable[..., Any]],
|
|
133
|
+
*,
|
|
134
|
+
router: Any | None = None,
|
|
135
|
+
warn_no_db: bool = False,
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Returns a ASGI endpoint function for /healthz.
|
|
139
|
+
If `dep` is provided, it's used as a dependency to supply `db`.
|
|
140
|
+
Otherwise, we try request.state.db.
|
|
141
|
+
"""
|
|
142
|
+
if dep is not None:
|
|
143
|
+
|
|
144
|
+
async def _healthz(db: Any = Depends(dep)):
|
|
145
|
+
if hasattr(db, "dependency") and callable(getattr(db, "dependency", None)):
|
|
146
|
+
resolved = db.dependency()
|
|
147
|
+
if inspect.isawaitable(resolved):
|
|
148
|
+
resolved = await resolved
|
|
149
|
+
db = resolved
|
|
150
|
+
entries = _resolve_db_entries(db, router)
|
|
151
|
+
if not entries:
|
|
152
|
+
if warn_no_db:
|
|
153
|
+
return {"ok": True, "warning": _NO_DB_WARNING}
|
|
154
|
+
return {"ok": True}
|
|
155
|
+
if len(entries) == 1:
|
|
156
|
+
result = await _check_db(*entries[0])
|
|
157
|
+
if result["ok"]:
|
|
158
|
+
return {"ok": True}
|
|
159
|
+
return JSONResponse(
|
|
160
|
+
{
|
|
161
|
+
"ok": False,
|
|
162
|
+
"warning": result["warning"],
|
|
163
|
+
"error": result["error"],
|
|
164
|
+
},
|
|
165
|
+
status_code=200,
|
|
166
|
+
)
|
|
167
|
+
results = [await _check_db(name, db) for name, db in entries]
|
|
168
|
+
return _multi_db_payload(results, include_all=False)
|
|
169
|
+
|
|
170
|
+
return _healthz
|
|
171
|
+
|
|
172
|
+
async def _healthz(request: Request):
|
|
173
|
+
entries = _resolve_db_entries(request, router)
|
|
174
|
+
if not entries:
|
|
175
|
+
if warn_no_db or _query_flag(request, "warn_no_db"):
|
|
176
|
+
return {"ok": True, "warning": _NO_DB_WARNING}
|
|
177
|
+
return {"ok": True}
|
|
178
|
+
if len(entries) == 1:
|
|
179
|
+
result = await _check_db(*entries[0])
|
|
180
|
+
if result["ok"]:
|
|
181
|
+
return {"ok": True}
|
|
182
|
+
return JSONResponse(
|
|
183
|
+
{
|
|
184
|
+
"ok": False,
|
|
185
|
+
"warning": result["warning"],
|
|
186
|
+
"error": result["error"],
|
|
187
|
+
},
|
|
188
|
+
status_code=200,
|
|
189
|
+
)
|
|
190
|
+
results = [await _check_db(name, db) for name, db in entries]
|
|
191
|
+
return _multi_db_payload(results, include_all=_wants_all_dbs(request))
|
|
192
|
+
|
|
193
|
+
return _healthz
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _with_leading_slash(path: str) -> str:
|
|
197
|
+
return path if path.startswith("/") else f"/{path}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def build_healthz_html(router: Any, request: Any, *, healthz_path: str | None = None) -> str:
|
|
201
|
+
"""Return a small operator-facing HTML page for the JSON health endpoint."""
|
|
202
|
+
|
|
203
|
+
base = (getattr(request, "script_name", "") or "").rstrip("/")
|
|
204
|
+
configured = healthz_path or f"{getattr(router, 'system_prefix', '/system')}/healthz"
|
|
205
|
+
payload_url = f"{base}{_with_leading_slash(configured)}"
|
|
206
|
+
title = getattr(router, "title", "API")
|
|
207
|
+
return f"""<!doctype html>
|
|
208
|
+
<html>
|
|
209
|
+
<head>
|
|
210
|
+
<meta charset="utf-8" />
|
|
211
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
212
|
+
<title>{title} health</title>
|
|
213
|
+
<style>
|
|
214
|
+
body {{
|
|
215
|
+
margin: 0;
|
|
216
|
+
min-height: 100vh;
|
|
217
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
218
|
+
color: #111827;
|
|
219
|
+
background: #f8fafc;
|
|
220
|
+
}}
|
|
221
|
+
main {{
|
|
222
|
+
width: min(720px, calc(100% - 32px));
|
|
223
|
+
margin: 0 auto;
|
|
224
|
+
padding: 48px 0;
|
|
225
|
+
}}
|
|
226
|
+
h1 {{
|
|
227
|
+
font-size: 24px;
|
|
228
|
+
font-weight: 650;
|
|
229
|
+
margin: 0 0 16px;
|
|
230
|
+
}}
|
|
231
|
+
pre {{
|
|
232
|
+
overflow: auto;
|
|
233
|
+
padding: 16px;
|
|
234
|
+
background: #111827;
|
|
235
|
+
color: #e5e7eb;
|
|
236
|
+
border-radius: 6px;
|
|
237
|
+
}}
|
|
238
|
+
</style>
|
|
239
|
+
</head>
|
|
240
|
+
<body>
|
|
241
|
+
<main>
|
|
242
|
+
<h1>Health</h1>
|
|
243
|
+
<pre id="payload">Loading {payload_url}</pre>
|
|
244
|
+
</main>
|
|
245
|
+
<script>
|
|
246
|
+
const payload = document.getElementById("payload");
|
|
247
|
+
fetch("{payload_url}", {{ headers: {{ "accept": "application/json" }} }})
|
|
248
|
+
.then((response) => response.json())
|
|
249
|
+
.then((data) => {{
|
|
250
|
+
payload.textContent = JSON.stringify(data, null, 2);
|
|
251
|
+
payload.dataset.ok = String(data.ok === true);
|
|
252
|
+
}})
|
|
253
|
+
.catch((error) => {{
|
|
254
|
+
payload.textContent = JSON.stringify({{ ok: false, error: String(error) }}, null, 2);
|
|
255
|
+
payload.dataset.ok = "false";
|
|
256
|
+
}});
|
|
257
|
+
</script>
|
|
258
|
+
</body>
|
|
259
|
+
</html>
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def mount_healthz_uix(
|
|
264
|
+
router: Any,
|
|
265
|
+
*,
|
|
266
|
+
path: str = "/healthz",
|
|
267
|
+
name: str = "__healthz_uix__",
|
|
268
|
+
healthz_path: str | None = None,
|
|
269
|
+
) -> Any:
|
|
270
|
+
"""Mount a human-viewable health page that reads the JSON health endpoint."""
|
|
271
|
+
|
|
272
|
+
def _healthz_uix_handler(request: Any) -> Response:
|
|
273
|
+
return Response.html(
|
|
274
|
+
build_healthz_html(router, request, healthz_path=healthz_path)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
router.add_route(
|
|
278
|
+
path,
|
|
279
|
+
_healthz_uix_handler,
|
|
280
|
+
methods=["GET"],
|
|
281
|
+
name=name,
|
|
282
|
+
include_in_schema=False,
|
|
283
|
+
inherit_owner_dependencies=False,
|
|
284
|
+
)
|
|
285
|
+
return router
|
|
@@ -8,16 +8,6 @@ from tigrbl_concrete.decorators import allow_anon
|
|
|
8
8
|
from tigrbl_typing import types as _typing_types
|
|
9
9
|
from tigrbl_typing.types import __all__ as _typing_all
|
|
10
10
|
|
|
11
|
-
_DEPRECATED_NAMES = {
|
|
12
|
-
"Router": "tigrbl",
|
|
13
|
-
"Request": "tigrbl",
|
|
14
|
-
"Body": "tigrbl.core.crud",
|
|
15
|
-
"Depends": "tigrbl.security",
|
|
16
|
-
"HTTPException": "tigrbl.runtime.status",
|
|
17
|
-
"Response": "tigrbl",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
11
|
for _name in _typing_all:
|
|
22
12
|
globals()[_name] = getattr(_typing_types, _name)
|
|
23
13
|
|
|
@@ -26,11 +16,5 @@ __all__ = [*_typing_all, "F", "IO", "S", "acol", "allow_anon"]
|
|
|
26
16
|
|
|
27
17
|
|
|
28
18
|
def __getattr__(name: str):
|
|
29
|
-
if name in _DEPRECATED_NAMES:
|
|
30
|
-
module = _DEPRECATED_NAMES[name]
|
|
31
|
-
raise AttributeError(
|
|
32
|
-
f"tigrbl.types no longer exports '{name}'. "
|
|
33
|
-
f"Import it from '{module}' instead."
|
|
34
|
-
)
|
|
35
19
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
36
20
|
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import inspect
|
|
4
|
-
import logging
|
|
5
|
-
from typing import Any, Callable, Optional
|
|
6
|
-
|
|
7
|
-
from ... import Depends, Request
|
|
8
|
-
from ..._concrete import JSONResponse
|
|
9
|
-
from .utils import maybe_execute
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _resolve_db(candidate: Any) -> Any:
|
|
15
|
-
"""Resolve a DB-like object from either a DB handle or a Request object."""
|
|
16
|
-
if hasattr(candidate, "execute"):
|
|
17
|
-
return candidate
|
|
18
|
-
|
|
19
|
-
state = getattr(candidate, "state", None)
|
|
20
|
-
db = getattr(state, "db", None)
|
|
21
|
-
if db is not None:
|
|
22
|
-
return db
|
|
23
|
-
return None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def build_healthz_endpoint(dep: Optional[Callable[..., Any]]):
|
|
27
|
-
"""
|
|
28
|
-
Returns a ASGI endpoint function for /healthz.
|
|
29
|
-
If `dep` is provided, it's used as a dependency to supply `db`.
|
|
30
|
-
Otherwise, we try request.state.db.
|
|
31
|
-
"""
|
|
32
|
-
if dep is not None:
|
|
33
|
-
|
|
34
|
-
async def _healthz(db: Any = Depends(dep)):
|
|
35
|
-
if hasattr(db, "dependency") and callable(getattr(db, "dependency", None)):
|
|
36
|
-
resolved = db.dependency()
|
|
37
|
-
if inspect.isawaitable(resolved):
|
|
38
|
-
resolved = await resolved
|
|
39
|
-
db = resolved
|
|
40
|
-
db = _resolve_db(db)
|
|
41
|
-
if db is None:
|
|
42
|
-
return {"ok": True, "warning": "no-db"}
|
|
43
|
-
try:
|
|
44
|
-
await maybe_execute(db, "SELECT 1")
|
|
45
|
-
return {"ok": True}
|
|
46
|
-
except Exception as e: # pragma: no cover
|
|
47
|
-
logger.warning("/healthz degraded: %s", e)
|
|
48
|
-
return JSONResponse(
|
|
49
|
-
{"ok": False, "warning": "db-unavailable", "error": str(e)},
|
|
50
|
-
status_code=200,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
return _healthz
|
|
54
|
-
|
|
55
|
-
async def _healthz(request: Request):
|
|
56
|
-
db = _resolve_db(request)
|
|
57
|
-
if db is None:
|
|
58
|
-
return {"ok": True, "warning": "no-db"}
|
|
59
|
-
try:
|
|
60
|
-
await maybe_execute(db, "SELECT 1")
|
|
61
|
-
return {"ok": True}
|
|
62
|
-
except Exception as e: # pragma: no cover
|
|
63
|
-
logger.warning("/healthz degraded: %s", e)
|
|
64
|
-
return JSONResponse(
|
|
65
|
-
{"ok": False, "warning": "db-unavailable", "error": str(e)},
|
|
66
|
-
status_code=200,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return _healthz
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|