tina4-python 3.10.40__tar.gz → 3.10.42__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.
- {tina4_python-3.10.40 → tina4_python-3.10.42}/PKG-INFO +53 -36
- {tina4_python-3.10.40 → tina4_python-3.10.42}/README.md +51 -34
- {tina4_python-3.10.40 → tina4_python-3.10.42}/pyproject.toml +2 -2
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/CLAUDE.md +2 -2
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/__init__.py +2 -2
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/server.py +69 -70
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dev_admin/__init__.py +9 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dev_admin/metrics.py +18 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/orm/model.py +7 -8
- {tina4_python-3.10.40 → tina4_python-3.10.42}/.gitignore +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/request.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/response.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/core/router.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tina4-python
|
|
3
|
-
Version: 3.10.
|
|
4
|
-
Summary: Tina4 Python
|
|
3
|
+
Version: 3.10.42
|
|
4
|
+
Summary: Tina4 for Python — 54 built-in features, zero dependencies
|
|
5
5
|
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.12
|
|
@@ -35,16 +35,15 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
</p>
|
|
36
36
|
|
|
37
37
|
<h1 align="center">Tina4 Python</h1>
|
|
38
|
-
<h3 align="center">This Is Now A 4Framework</h3>
|
|
39
38
|
|
|
40
39
|
<p align="center">
|
|
41
|
-
|
|
40
|
+
54 built-in features. Zero dependencies. One import, everything works.
|
|
42
41
|
</p>
|
|
43
42
|
|
|
44
43
|
<p align="center">
|
|
45
44
|
<a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
|
|
46
|
-
<img src="https://img.shields.io/badge/tests-
|
|
47
|
-
<img src="https://img.shields.io/badge/features-
|
|
45
|
+
<img src="https://img.shields.io/badge/tests-2%2C068%20passing-brightgreen" alt="Tests">
|
|
46
|
+
<img src="https://img.shields.io/badge/features-54-blue" alt="Features">
|
|
48
47
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
49
48
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
50
49
|
</p>
|
|
@@ -54,6 +53,7 @@ Description-Content-Type: text/markdown
|
|
|
54
53
|
<a href="#getting-started">Getting Started</a> •
|
|
55
54
|
<a href="#features">Features</a> •
|
|
56
55
|
<a href="#cli-reference">CLI Reference</a> •
|
|
56
|
+
<a href="#cross-framework-parity">Parity</a> •
|
|
57
57
|
<a href="https://tina4.com">tina4.com</a>
|
|
58
58
|
</p>
|
|
59
59
|
|
|
@@ -102,27 +102,24 @@ Open http://localhost:7145
|
|
|
102
102
|
|
|
103
103
|
---
|
|
104
104
|
|
|
105
|
-
## What's
|
|
105
|
+
## What's Built In (54 Features)
|
|
106
106
|
|
|
107
107
|
Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
|
|
108
108
|
|
|
109
109
|
| Category | Features |
|
|
110
110
|
|----------|----------|
|
|
111
|
-
| **HTTP** |
|
|
112
|
-
| **
|
|
113
|
-
| **ORM** | Active Record
|
|
114
|
-
| **
|
|
115
|
-
| **
|
|
116
|
-
| **API** |
|
|
117
|
-
| **Background** |
|
|
118
|
-
| **
|
|
119
|
-
| **
|
|
120
|
-
| **
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages, HTML element builder |
|
|
124
|
-
|
|
125
|
-
**1,633 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
|
|
111
|
+
| **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
|
|
112
|
+
| **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird — unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
|
|
113
|
+
| **ORM** (7) | Active Record with typed fields, relationships (`has_one`/`has_many`/`belongs_to`), soft delete, QueryBuilder + MongoDB support, Auto-CRUD generator, migrations with rollback |
|
|
114
|
+
| **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
|
|
115
|
+
| **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8x faster), SCSS auto-compilation, built-in CSS (~24 KB) |
|
|
116
|
+
| **API & Integration** (5) | HTTP client (zero-dep), GraphQL with ORM auto-schema + GraphiQL IDE, WSDL/SOAP with auto WSDL, WebSocket (RFC 6455) + Redis backplane, MCP server (24 dev tools) |
|
|
117
|
+
| **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters — service runner — event system (on/emit/once/off) |
|
|
118
|
+
| **Data & Storage** (4) | Session (File/Redis/Valkey/MongoDB/DB), response cache (LRU, TTL), seeder + 50+ fake data generators, messenger (SMTP/IMAP) |
|
|
119
|
+
| **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
|
|
120
|
+
| **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
|
|
121
|
+
|
|
122
|
+
**2,066 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
|
|
126
123
|
|
|
127
124
|
For full documentation visit **[tina4.com](https://tina4.com)**.
|
|
128
125
|
|
|
@@ -712,23 +709,43 @@ tina4python ai --all # Install for ALL supported tools
|
|
|
712
709
|
|
|
713
710
|
Supported: Claude Code, Cursor, GitHub Copilot, Windsurf, Aider, Cline, OpenAI Codex CLI. Generates framework-aware context so AI assistants understand Tina4's conventions.
|
|
714
711
|
|
|
715
|
-
##
|
|
712
|
+
## Performance
|
|
716
713
|
|
|
717
|
-
|
|
714
|
+
Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
|
|
718
715
|
|
|
719
|
-
|
|
|
720
|
-
|
|
721
|
-
|
|
|
722
|
-
|
|
|
723
|
-
|
|
|
724
|
-
|
|
|
725
|
-
|
|
|
726
|
-
| Plaintext Response | 0.000377 | A+ |
|
|
727
|
-
| CRUD Cycle | 0.000456 | A+ |
|
|
728
|
-
| Paginated Query | 0.000990 | A+ |
|
|
729
|
-
| Framework Startup | 0.000801 | A+ |
|
|
716
|
+
| Framework | JSON req/s | Deps | Features |
|
|
717
|
+
|-----------|-----------|------|----------|
|
|
718
|
+
| **Tina4 Python** | **6,508** | 0 | 54 |
|
|
719
|
+
| FastAPI | 12,652 | 12+ | ~8 |
|
|
720
|
+
| Flask | 4,928 | 6+ | ~7 |
|
|
721
|
+
| Bottle | 4,355 | 0 | ~5 |
|
|
722
|
+
| Django | 4,050 | 20+ | ~22 |
|
|
730
723
|
|
|
731
|
-
|
|
724
|
+
Tina4 Python delivers competitive throughput with **zero dependencies and 54 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
|
|
725
|
+
|
|
726
|
+
**Across all 4 Tina4 implementations:**
|
|
727
|
+
|
|
728
|
+
| | Python | PHP | Ruby | Node.js |
|
|
729
|
+
|---|--------|-----|------|---------|
|
|
730
|
+
| **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
|
|
731
|
+
| **Dependencies** | 0 | 0 | 0 | 0 |
|
|
732
|
+
| **Features** | 54 | 54 | 54 | 54 |
|
|
733
|
+
|
|
734
|
+
Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Cross-Framework Parity
|
|
739
|
+
|
|
740
|
+
Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
|
|
741
|
+
|
|
742
|
+
| | Python | PHP | Ruby | Node.js |
|
|
743
|
+
|---|--------|-----|------|---------|
|
|
744
|
+
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
|
|
745
|
+
| **Tests** | 2,066 | 1,427 | 1,793 | 1,950 |
|
|
746
|
+
| **Default port** | 7145 | 7146 | 7147 | 7148 |
|
|
747
|
+
|
|
748
|
+
**7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
|
|
732
749
|
|
|
733
750
|
---
|
|
734
751
|
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Tina4 Python</h1>
|
|
6
|
-
<h3 align="center">This Is Now A 4Framework</h3>
|
|
7
6
|
|
|
8
7
|
<p align="center">
|
|
9
|
-
|
|
8
|
+
54 built-in features. Zero dependencies. One import, everything works.
|
|
10
9
|
</p>
|
|
11
10
|
|
|
12
11
|
<p align="center">
|
|
13
12
|
<a href="https://pypi.org/project/tina4-python/"><img src="https://img.shields.io/pypi/v/tina4-python?color=7b1fa2&label=PyPI" alt="PyPI"></a>
|
|
14
|
-
<img src="https://img.shields.io/badge/tests-
|
|
15
|
-
<img src="https://img.shields.io/badge/features-
|
|
13
|
+
<img src="https://img.shields.io/badge/tests-2%2C068%20passing-brightgreen" alt="Tests">
|
|
14
|
+
<img src="https://img.shields.io/badge/features-54-blue" alt="Features">
|
|
16
15
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
|
|
17
16
|
<a href="https://tina4.com"><img src="https://img.shields.io/badge/docs-tina4.com-7b1fa2" alt="Docs"></a>
|
|
18
17
|
</p>
|
|
@@ -22,6 +21,7 @@
|
|
|
22
21
|
<a href="#getting-started">Getting Started</a> •
|
|
23
22
|
<a href="#features">Features</a> •
|
|
24
23
|
<a href="#cli-reference">CLI Reference</a> •
|
|
24
|
+
<a href="#cross-framework-parity">Parity</a> •
|
|
25
25
|
<a href="https://tina4.com">tina4.com</a>
|
|
26
26
|
</p>
|
|
27
27
|
|
|
@@ -70,27 +70,24 @@ Open http://localhost:7145
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
## What's
|
|
73
|
+
## What's Built In (54 Features)
|
|
74
74
|
|
|
75
75
|
Every feature is built from scratch -- no pip install, no node_modules, no third-party runtime dependencies in core.
|
|
76
76
|
|
|
77
77
|
| Category | Features |
|
|
78
78
|
|----------|----------|
|
|
79
|
-
| **HTTP** |
|
|
80
|
-
| **
|
|
81
|
-
| **ORM** | Active Record
|
|
82
|
-
| **
|
|
83
|
-
| **
|
|
84
|
-
| **API** |
|
|
85
|
-
| **Background** |
|
|
86
|
-
| **
|
|
87
|
-
| **
|
|
88
|
-
| **
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| **Other** | REST client, localization (6 languages), cache (memory/Redis/file), event system, inline testing, messenger (.env driven), configurable error pages, HTML element builder |
|
|
92
|
-
|
|
93
|
-
**1,633 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
|
|
79
|
+
| **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
|
|
80
|
+
| **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird — unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
|
|
81
|
+
| **ORM** (7) | Active Record with typed fields, relationships (`has_one`/`has_many`/`belongs_to`), soft delete, QueryBuilder + MongoDB support, Auto-CRUD generator, migrations with rollback |
|
|
82
|
+
| **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
|
|
83
|
+
| **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8x faster), SCSS auto-compilation, built-in CSS (~24 KB) |
|
|
84
|
+
| **API & Integration** (5) | HTTP client (zero-dep), GraphQL with ORM auto-schema + GraphiQL IDE, WSDL/SOAP with auto WSDL, WebSocket (RFC 6455) + Redis backplane, MCP server (24 dev tools) |
|
|
85
|
+
| **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters — service runner — event system (on/emit/once/off) |
|
|
86
|
+
| **Data & Storage** (4) | Session (File/Redis/Valkey/MongoDB/DB), response cache (LRU, TTL), seeder + 50+ fake data generators, messenger (SMTP/IMAP) |
|
|
87
|
+
| **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
|
|
88
|
+
| **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
|
|
89
|
+
|
|
90
|
+
**2,066 tests. Zero dependencies. Full parity across Python, PHP, Ruby, and Node.js.**
|
|
94
91
|
|
|
95
92
|
For full documentation visit **[tina4.com](https://tina4.com)**.
|
|
96
93
|
|
|
@@ -680,23 +677,43 @@ tina4python ai --all # Install for ALL supported tools
|
|
|
680
677
|
|
|
681
678
|
Supported: Claude Code, Cursor, GitHub Copilot, Windsurf, Aider, Cline, OpenAI Codex CLI. Generates framework-aware context so AI assistants understand Tina4's conventions.
|
|
682
679
|
|
|
683
|
-
##
|
|
680
|
+
## Performance
|
|
684
681
|
|
|
685
|
-
|
|
682
|
+
Benchmarked with `wrk` — 5,000 requests, 50 concurrent, median of 3 runs:
|
|
686
683
|
|
|
687
|
-
|
|
|
688
|
-
|
|
689
|
-
|
|
|
690
|
-
|
|
|
691
|
-
|
|
|
692
|
-
|
|
|
693
|
-
|
|
|
694
|
-
| Plaintext Response | 0.000377 | A+ |
|
|
695
|
-
| CRUD Cycle | 0.000456 | A+ |
|
|
696
|
-
| Paginated Query | 0.000990 | A+ |
|
|
697
|
-
| Framework Startup | 0.000801 | A+ |
|
|
684
|
+
| Framework | JSON req/s | Deps | Features |
|
|
685
|
+
|-----------|-----------|------|----------|
|
|
686
|
+
| **Tina4 Python** | **6,508** | 0 | 54 |
|
|
687
|
+
| FastAPI | 12,652 | 12+ | ~8 |
|
|
688
|
+
| Flask | 4,928 | 6+ | ~7 |
|
|
689
|
+
| Bottle | 4,355 | 0 | ~5 |
|
|
690
|
+
| Django | 4,050 | 20+ | ~22 |
|
|
698
691
|
|
|
699
|
-
|
|
692
|
+
Tina4 Python delivers competitive throughput with **zero dependencies and 54 features** — frameworks with higher req/s have a fraction of the functionality and require dozens of third-party packages.
|
|
693
|
+
|
|
694
|
+
**Across all 4 Tina4 implementations:**
|
|
695
|
+
|
|
696
|
+
| | Python | PHP | Ruby | Node.js |
|
|
697
|
+
|---|--------|-----|------|---------|
|
|
698
|
+
| **JSON req/s** | 6,508 | 29,293 | 10,243 | 84,771 |
|
|
699
|
+
| **Dependencies** | 0 | 0 | 0 | 0 |
|
|
700
|
+
| **Features** | 54 | 54 | 54 | 54 |
|
|
701
|
+
|
|
702
|
+
Run benchmarks locally: `python benchmarks/benchmark.py --python`
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## Cross-Framework Parity
|
|
707
|
+
|
|
708
|
+
Tina4 ships identical features across four languages — same architecture, same conventions, same 54 features:
|
|
709
|
+
|
|
710
|
+
| | Python | PHP | Ruby | Node.js |
|
|
711
|
+
|---|--------|-----|------|---------|
|
|
712
|
+
| **Package** | `tina4-python` | `tina4stack/tina4php` | `tina4ruby` | `tina4-nodejs` |
|
|
713
|
+
| **Tests** | 2,066 | 1,427 | 1,793 | 1,950 |
|
|
714
|
+
| **Default port** | 7145 | 7146 | 7147 | 7148 |
|
|
715
|
+
|
|
716
|
+
**7,236 tests** across all 4 frameworks. See [tina4.com](https://tina4.com).
|
|
700
717
|
|
|
701
718
|
---
|
|
702
719
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tina4-python"
|
|
3
|
-
version = "3.10.
|
|
4
|
-
description = "Tina4 Python
|
|
3
|
+
version = "3.10.42"
|
|
4
|
+
description = "Tina4 for Python — 54 built-in features, zero dependencies"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
|
|
7
7
|
]
|
|
@@ -1762,8 +1762,8 @@ async def dashboard(request, response):
|
|
|
1762
1762
|
|
|
1763
1763
|
## v3 Features Summary
|
|
1764
1764
|
|
|
1765
|
-
- **
|
|
1766
|
-
- **
|
|
1765
|
+
- **54 built-in features**, zero third-party dependencies
|
|
1766
|
+
- **2,066 tests** passing across all modules
|
|
1767
1767
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1768
1768
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
1769
1769
|
- **Database**: 5 engines (SQLite, PostgreSQL, MySQL, MSSQL, Firebird), query caching (`TINA4_DB_CACHE=true`, `cache_stats()`, `cache_clear()`)
|
|
@@ -19,7 +19,7 @@ from tina4_python.core.router import (
|
|
|
19
19
|
from tina4_python.core.middleware import CorsMiddleware, RateLimiter
|
|
20
20
|
from tina4_python.core.cache import Cache
|
|
21
21
|
from tina4_python.core.events import on, off, emit, emit_async, once, listeners, events, clear as clear_events
|
|
22
|
-
from tina4_python.core.server import run, resolve_config
|
|
22
|
+
from tina4_python.core.server import run, resolve_config, handle
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
"Request", "Response", "Router",
|
|
@@ -28,5 +28,5 @@ __all__ = [
|
|
|
28
28
|
"CorsMiddleware", "RateLimiter",
|
|
29
29
|
"Cache",
|
|
30
30
|
"on", "off", "emit", "emit_async", "once", "listeners", "events", "clear_events",
|
|
31
|
-
"run", "resolve_config",
|
|
31
|
+
"run", "resolve_config", "handle",
|
|
32
32
|
]
|
|
@@ -607,54 +607,33 @@ async def _handle_dev_websocket(reader, writer, headers, path):
|
|
|
607
607
|
pass
|
|
608
608
|
|
|
609
609
|
|
|
610
|
-
async def app(scope: dict, receive, send):
|
|
611
|
-
"""ASGI entry point — compatible with uvicorn, hypercorn, granian."""
|
|
612
|
-
if scope["type"] == "lifespan":
|
|
613
|
-
msg = await receive()
|
|
614
|
-
if msg["type"] == "lifespan.startup":
|
|
615
|
-
import time
|
|
616
|
-
global _start_time
|
|
617
|
-
_start_time = time.time()
|
|
618
|
-
await send({"type": "lifespan.startup.complete"})
|
|
619
|
-
elif msg["type"] == "lifespan.shutdown":
|
|
620
|
-
await send({"type": "lifespan.shutdown.complete"})
|
|
621
|
-
return
|
|
622
|
-
|
|
623
|
-
if scope["type"] == "websocket":
|
|
624
|
-
await _handle_asgi_websocket(scope, receive, send)
|
|
625
|
-
return
|
|
626
|
-
|
|
627
|
-
if scope["type"] != "http":
|
|
628
|
-
return
|
|
629
610
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
while True:
|
|
633
|
-
msg = await receive()
|
|
634
|
-
body += msg.get("body", b"")
|
|
635
|
-
if not msg.get("more_body", False):
|
|
636
|
-
break
|
|
611
|
+
async def handle(request: Request) -> Response:
|
|
612
|
+
"""Dispatch a pre-built Request through the Tina4 router and return a Response.
|
|
637
613
|
|
|
638
|
-
|
|
639
|
-
|
|
614
|
+
Handles session setup, CORS, rate limiting, routing, auth, middleware,
|
|
615
|
+
dev toolbar injection, and session saving. The caller is responsible
|
|
616
|
+
for sending the response over the wire. Useful for testing and embedding.
|
|
617
|
+
"""
|
|
640
618
|
request_id = request.headers.get("x-request-id", str(uuid.uuid4())[:8])
|
|
641
619
|
set_request_id(request_id)
|
|
642
620
|
|
|
643
621
|
# Auto-start session — lazy, reads cookie, saves on response
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
part
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
622
|
+
if request.session is None:
|
|
623
|
+
try:
|
|
624
|
+
from tina4_python.session import Session
|
|
625
|
+
cookie_header = request.headers.get("cookie", "")
|
|
626
|
+
sid_match = None
|
|
627
|
+
for part in cookie_header.split(";"):
|
|
628
|
+
part = part.strip()
|
|
629
|
+
if part.startswith("tina4_session="):
|
|
630
|
+
sid_match = part.split("=", 1)[1]
|
|
631
|
+
break
|
|
632
|
+
sess = Session()
|
|
633
|
+
sess.start(sid_match)
|
|
634
|
+
request.session = sess
|
|
635
|
+
except Exception:
|
|
636
|
+
pass # Session module not available — session stays None
|
|
658
637
|
|
|
659
638
|
response = Response()
|
|
660
639
|
response.header("x-request-id", request_id)
|
|
@@ -663,9 +642,7 @@ async def app(scope: dict, receive, send):
|
|
|
663
642
|
if _cors.is_preflight(request):
|
|
664
643
|
_cors.apply(request, response)
|
|
665
644
|
response.status(204)
|
|
666
|
-
|
|
667
|
-
await send({"type": "http.response.body", "body": b""})
|
|
668
|
-
return
|
|
645
|
+
return response
|
|
669
646
|
|
|
670
647
|
# Rate limiting
|
|
671
648
|
rate_enabled = os.environ.get("TINA4_RATE_LIMIT", "")
|
|
@@ -680,9 +657,7 @@ async def app(scope: dict, receive, send):
|
|
|
680
657
|
"status": 429,
|
|
681
658
|
})
|
|
682
659
|
response.header("retry-after", str(info["reset"]))
|
|
683
|
-
|
|
684
|
-
await send({"type": "http.response.body", "body": response.content})
|
|
685
|
-
return
|
|
660
|
+
return response
|
|
686
661
|
|
|
687
662
|
import time as _time
|
|
688
663
|
_req_start = _time.perf_counter()
|
|
@@ -727,12 +702,9 @@ async def app(scope: dict, receive, send):
|
|
|
727
702
|
else:
|
|
728
703
|
response.status(404).json({"error": "Not found"})
|
|
729
704
|
|
|
730
|
-
#
|
|
705
|
+
# Dev admin response (skip overlay injection)
|
|
731
706
|
_cors.apply(request, response)
|
|
732
|
-
|
|
733
|
-
await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
|
|
734
|
-
await send({"type": "http.response.body", "body": response.content})
|
|
735
|
-
return
|
|
707
|
+
return response
|
|
736
708
|
|
|
737
709
|
# Swagger auto-register: serve /swagger and /swagger/openapi.json when debug is on
|
|
738
710
|
if _is_dev and request.method == "GET":
|
|
@@ -749,10 +721,7 @@ async def app(scope: dict, receive, send):
|
|
|
749
721
|
)
|
|
750
722
|
response.html(swagger_html)
|
|
751
723
|
_cors.apply(request, response)
|
|
752
|
-
|
|
753
|
-
await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
|
|
754
|
-
await send({"type": "http.response.body", "body": response.content})
|
|
755
|
-
return
|
|
724
|
+
return response
|
|
756
725
|
elif request.path == "/swagger/openapi.json":
|
|
757
726
|
# Serve OpenAPI spec JSON from all registered routes
|
|
758
727
|
from tina4_python.swagger import Swagger as _SwaggerGen
|
|
@@ -760,10 +729,7 @@ async def app(scope: dict, receive, send):
|
|
|
760
729
|
_spec = _swagger.generate(Router.get_routes())
|
|
761
730
|
response.json(_spec)
|
|
762
731
|
_cors.apply(request, response)
|
|
763
|
-
|
|
764
|
-
await send({"type": "http.response.start", "status": response.status_code, "headers": headers})
|
|
765
|
-
await send({"type": "http.response.body", "body": response.content})
|
|
766
|
-
return
|
|
732
|
+
return response
|
|
767
733
|
|
|
768
734
|
# Match route
|
|
769
735
|
route, params = Router.match(request.method, request.path)
|
|
@@ -931,13 +897,13 @@ async def app(scope: dict, receive, send):
|
|
|
931
897
|
request.method, request.path, matched_pattern,
|
|
932
898
|
request_id, len(Router.get_routes()),
|
|
933
899
|
).encode()
|
|
934
|
-
|
|
900
|
+
content_body = response.content
|
|
935
901
|
# Inject before </body> if present, else append
|
|
936
|
-
if b"</body>" in
|
|
937
|
-
|
|
902
|
+
if b"</body>" in content_body:
|
|
903
|
+
content_body = content_body.replace(b"</body>", toolbar + b"\n</body>", 1)
|
|
938
904
|
else:
|
|
939
|
-
|
|
940
|
-
response.content =
|
|
905
|
+
content_body = content_body + toolbar
|
|
906
|
+
response.content = content_body
|
|
941
907
|
except Exception:
|
|
942
908
|
pass
|
|
943
909
|
|
|
@@ -954,9 +920,6 @@ async def app(scope: dict, receive, send):
|
|
|
954
920
|
except Exception:
|
|
955
921
|
pass
|
|
956
922
|
|
|
957
|
-
# ETag check — 304 Not Modified
|
|
958
|
-
if_none_match = request.headers.get("if-none-match", "")
|
|
959
|
-
|
|
960
923
|
# Save session and set cookie if session was used
|
|
961
924
|
if request.session is not None:
|
|
962
925
|
try:
|
|
@@ -973,7 +936,43 @@ async def app(scope: dict, receive, send):
|
|
|
973
936
|
except Exception:
|
|
974
937
|
pass
|
|
975
938
|
|
|
976
|
-
|
|
939
|
+
return response
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
async def app(scope: dict, receive, send):
|
|
943
|
+
"""ASGI entry point — compatible with uvicorn, hypercorn, granian."""
|
|
944
|
+
if scope["type"] == "lifespan":
|
|
945
|
+
msg = await receive()
|
|
946
|
+
if msg["type"] == "lifespan.startup":
|
|
947
|
+
import time
|
|
948
|
+
global _start_time
|
|
949
|
+
_start_time = time.time()
|
|
950
|
+
await send({"type": "lifespan.startup.complete"})
|
|
951
|
+
elif msg["type"] == "lifespan.shutdown":
|
|
952
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
953
|
+
return
|
|
954
|
+
|
|
955
|
+
if scope["type"] == "websocket":
|
|
956
|
+
await _handle_asgi_websocket(scope, receive, send)
|
|
957
|
+
return
|
|
958
|
+
|
|
959
|
+
if scope["type"] != "http":
|
|
960
|
+
return
|
|
961
|
+
|
|
962
|
+
# Read full body
|
|
963
|
+
body = b""
|
|
964
|
+
while True:
|
|
965
|
+
msg = await receive()
|
|
966
|
+
body += msg.get("body", b"")
|
|
967
|
+
if not msg.get("more_body", False):
|
|
968
|
+
break
|
|
969
|
+
|
|
970
|
+
# Build request and dispatch
|
|
971
|
+
request = Request.from_scope(scope, body)
|
|
972
|
+
response = await handle(request)
|
|
973
|
+
|
|
974
|
+
# ETag check — 304 Not Modified
|
|
975
|
+
if_none_match = request.headers.get("if-none-match", "")
|
|
977
976
|
accept_encoding = request.headers.get("accept-encoding", "")
|
|
978
977
|
headers = response.build_headers(accept_encoding)
|
|
979
978
|
|
|
@@ -1924,6 +1924,15 @@ function drillDownFile(path){
|
|
|
1924
1924
|
});
|
|
1925
1925
|
html+='</div>';
|
|
1926
1926
|
}
|
|
1927
|
+
if(d.warnings&&d.warnings.length){
|
|
1928
|
+
html+='<h3 style="margin:0.75rem 0 0.25rem;color:#eab308;font-size:0.85rem">⚠ Warnings</h3>';
|
|
1929
|
+
html+='<div style="display:flex;flex-direction:column;gap:4px">';
|
|
1930
|
+
d.warnings.forEach(function(w){
|
|
1931
|
+
html+='<div style="padding:4px 8px;background:rgba(234,179,8,0.08);border-left:3px solid #eab308;border-radius:0 4px 4px 0;font-size:0.75rem;font-family:var(--mono);color:var(--text)">';
|
|
1932
|
+
html+='<span style="color:#eab308;margin-right:6px">L'+w.line+'</span>'+w.message+'</div>';
|
|
1933
|
+
});
|
|
1934
|
+
html+='</div>';
|
|
1935
|
+
}
|
|
1927
1936
|
dd.querySelector('.p-md').innerHTML=html;
|
|
1928
1937
|
}).catch(function(e){
|
|
1929
1938
|
dd.querySelector('.p-md').innerHTML='<p style="color:var(--danger)">Error: '+e.message+'</p>';
|
|
@@ -327,6 +327,23 @@ def file_detail(file_path: str) -> dict:
|
|
|
327
327
|
classes = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
|
|
328
328
|
imports = _extract_imports(tree, file_path)
|
|
329
329
|
|
|
330
|
+
# Detect empty methods/functions (body is only `pass` or a docstring)
|
|
331
|
+
warnings = []
|
|
332
|
+
for node in ast.walk(tree):
|
|
333
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
334
|
+
body = node.body
|
|
335
|
+
# Strip leading docstring
|
|
336
|
+
effective = body[1:] if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) else body
|
|
337
|
+
if not effective or all(isinstance(s, ast.Pass) for s in effective):
|
|
338
|
+
parent = _get_parent_class(tree, node)
|
|
339
|
+
name = f"{parent}.{node.name}" if parent else node.name
|
|
340
|
+
warnings.append({"type": "empty_method", "message": f"Method '{name}' appears to be empty", "line": node.lineno})
|
|
341
|
+
elif isinstance(node, ast.ClassDef):
|
|
342
|
+
body = node.body
|
|
343
|
+
effective = body[1:] if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) else body
|
|
344
|
+
if not effective or all(isinstance(s, ast.Pass) for s in effective):
|
|
345
|
+
warnings.append({"type": "empty_class", "message": f"Class '{node.name}' appears to be empty", "line": node.lineno})
|
|
346
|
+
|
|
330
347
|
return {
|
|
331
348
|
"path": file_path,
|
|
332
349
|
"loc": loc,
|
|
@@ -334,6 +351,7 @@ def file_detail(file_path: str) -> dict:
|
|
|
334
351
|
"classes": classes,
|
|
335
352
|
"functions": functions,
|
|
336
353
|
"imports": imports,
|
|
354
|
+
"warnings": warnings,
|
|
337
355
|
}
|
|
338
356
|
|
|
339
357
|
|
|
@@ -325,7 +325,6 @@ class ORM(metaclass=ORMMeta):
|
|
|
325
325
|
pk_value: Primary key value.
|
|
326
326
|
include: List of relationship names to eager-load.
|
|
327
327
|
"""
|
|
328
|
-
db = cls._get_db()
|
|
329
328
|
pk = cls._get_pk()
|
|
330
329
|
table = cls._get_table()
|
|
331
330
|
pk_col = cls.field_mapping.get(pk, cls._fields[pk].column)
|
|
@@ -334,13 +333,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
334
333
|
if cls.soft_delete:
|
|
335
334
|
sql += " AND deleted_at IS NULL"
|
|
336
335
|
|
|
337
|
-
|
|
338
|
-
if row is None:
|
|
339
|
-
return None
|
|
340
|
-
instance = cls(row)
|
|
341
|
-
if include:
|
|
342
|
-
cls._eager_load([instance], include)
|
|
343
|
-
return instance
|
|
336
|
+
return cls.select_one(sql, [pk_value], include=include)
|
|
344
337
|
|
|
345
338
|
@classmethod
|
|
346
339
|
def find(cls, pk_value, include: list[str] = None):
|
|
@@ -395,6 +388,12 @@ class ORM(metaclass=ORMMeta):
|
|
|
395
388
|
cls._eager_load(instances, include)
|
|
396
389
|
return instances, result.count
|
|
397
390
|
|
|
391
|
+
@classmethod
|
|
392
|
+
def select_one(cls, sql: str, params: list = None, include: list[str] = None):
|
|
393
|
+
"""Return a single ORM instance for a raw SQL query, or None if no rows match."""
|
|
394
|
+
instances, _ = cls.select(sql, params, limit=1, offset=0, include=include)
|
|
395
|
+
return instances[0] if instances else None
|
|
396
|
+
|
|
398
397
|
@classmethod
|
|
399
398
|
def where(cls, filter_sql: str, params: list = None, limit: int = 20, offset: int = 0,
|
|
400
399
|
include: list[str] = None):
|
|
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
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
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
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
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
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.40 → tina4_python-3.10.42}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|