tina4-python 0.2.201__tar.gz → 0.2.203__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-0.2.201/README.md → tina4_python-0.2.203/PKG-INFO +154 -22
- tina4_python-0.2.201/PKG-INFO → tina4_python-0.2.203/README.md +117 -43
- {tina4_python-0.2.201 → tina4_python-0.2.203}/pyproject.toml +18 -4
- tina4_python-0.2.203/tina4_python/Auth.py +236 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Messages.py +1 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Router.py +269 -76
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/__init__.py +20 -35
- tina4_python-0.2.201/tina4_python/Auth.py +0 -354
- {tina4_python-0.2.201 → tina4_python-0.2.203}/.gitignore +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Api.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/CRUD.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Constant.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Database.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/DatabaseResult.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/DatabaseTypes.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Debug.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/DevReload.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Env.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/FieldTypes.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/GraphQL.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Localization.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/MiddleWare.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Migration.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/ORM.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Queue.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Request.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Response.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/SQLToMongo.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Seeder.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Session.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/ShellColors.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Swagger.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Template.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Testing.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/WSDL.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Webserver.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/Websocket.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/cli.py +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/messages.pot +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/css/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/images/403.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/images/404.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/images/500.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/images/logo.png +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/images/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/js/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/js/reconnecting-websocket.js +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/js/tina4.js +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/js/tina4helper.js +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/templates/readme.md +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-0.2.201 → tina4_python-0.2.203}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tina4-python
|
|
3
|
+
Version: 0.2.203
|
|
4
|
+
Summary: Tina4Python - This is not another framework for Python
|
|
5
|
+
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
|
+
Requires-Python: <4.0,>=3.12
|
|
7
|
+
Requires-Dist: asyncer>=0.0.8
|
|
8
|
+
Requires-Dist: bcrypt<5.0.0,>=4.2.1
|
|
9
|
+
Requires-Dist: hypercorn>=0.18.0
|
|
10
|
+
Requires-Dist: jinja2<4.0.0,>=3.1.5
|
|
11
|
+
Requires-Dist: libsass<0.24.0,>=0.23.0
|
|
12
|
+
Requires-Dist: litequeue<0.10,>=0.9
|
|
13
|
+
Requires-Dist: pyjwt<3.0.0,>=2.10.1
|
|
14
|
+
Requires-Dist: python-dotenv<2.0.0,>=1.0.1
|
|
15
|
+
Requires-Dist: requests>=2.32.5
|
|
16
|
+
Requires-Dist: simple-websocket<2.0.0,>=1.1.0
|
|
17
|
+
Requires-Dist: watchdog<7.0.0,>=6.0.0
|
|
18
|
+
Provides-Extra: all-db
|
|
19
|
+
Requires-Dist: firebird-driver>=1.10.0; extra == 'all-db'
|
|
20
|
+
Requires-Dist: mysql-connector-python>=9.3.0; extra == 'all-db'
|
|
21
|
+
Requires-Dist: psycopg2-binary>=2.9.10; extra == 'all-db'
|
|
22
|
+
Requires-Dist: pymongo>=4.0.0; extra == 'all-db'
|
|
23
|
+
Requires-Dist: pymssql>=2.3.0; extra == 'all-db'
|
|
24
|
+
Provides-Extra: dev-reload
|
|
25
|
+
Requires-Dist: jurigged>=0.6.0; extra == 'dev-reload'
|
|
26
|
+
Provides-Extra: firebird
|
|
27
|
+
Requires-Dist: firebird-driver>=1.10.0; extra == 'firebird'
|
|
28
|
+
Provides-Extra: mongo
|
|
29
|
+
Requires-Dist: pymongo>=4.0.0; extra == 'mongo'
|
|
30
|
+
Provides-Extra: mssql
|
|
31
|
+
Requires-Dist: pymssql>=2.3.0; extra == 'mssql'
|
|
32
|
+
Provides-Extra: mysql
|
|
33
|
+
Requires-Dist: mysql-connector-python>=9.3.0; extra == 'mysql'
|
|
34
|
+
Provides-Extra: postgres
|
|
35
|
+
Requires-Dist: psycopg2-binary>=2.9.10; extra == 'postgres'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
1
38
|
# Tina4 Python — This is not a framework
|
|
2
39
|
|
|
3
40
|
Laravel joy. Python speed. 10x less code.
|
|
@@ -21,13 +58,16 @@ You've just built your first Tina4 app — zero configuration, zero classes, zer
|
|
|
21
58
|
- **Full async** — every route handler is `async` by default
|
|
22
59
|
- **Routing** — decorator-based with path parameters, type hints, and auto-discovery
|
|
23
60
|
- **Twig/Jinja2 templates** — with inheritance, partials, custom filters, and globals
|
|
61
|
+
- **tina4-css** — lightweight CSS framework (~24 KB) ships built-in, Bootstrap-compatible class names
|
|
24
62
|
- **ORM** — define models with typed fields, save/load/select/delete with one line
|
|
25
|
-
- **Database** — SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, Firebird
|
|
63
|
+
- **Database** — SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, Firebird, MongoDB
|
|
26
64
|
- **Migrations** — versioned SQL files, CLI scaffolding
|
|
65
|
+
- **Data seeder** — zero-dependency fake data generation with ORM and table support
|
|
27
66
|
- **Sessions** — file, Redis, Valkey, or MongoDB backends
|
|
28
|
-
- **JWT authentication** —
|
|
67
|
+
- **JWT authentication** — HS256 tokens signed with your `SECRET` env var, form tokens
|
|
29
68
|
- **Swagger/OpenAPI** — auto-generated docs at `/swagger`
|
|
30
|
-
- **CRUD scaffolding** — instant admin UI with one line of code
|
|
69
|
+
- **CRUD scaffolding** — instant searchable admin UI with one line of code
|
|
70
|
+
- **GraphQL** — zero-dependency engine with ORM auto-schema and GraphiQL IDE
|
|
31
71
|
- **Middleware** — before/after hooks per route or globally
|
|
32
72
|
- **Queues** — background processing with litequeue, RabbitMQ, Kafka, or MongoDB
|
|
33
73
|
- **WebSockets** — built-in support via `simple-websocket`
|
|
@@ -36,7 +76,7 @@ You've just built your first Tina4 app — zero configuration, zero classes, zer
|
|
|
36
76
|
- **SCSS compilation** — auto-compiled to CSS on save
|
|
37
77
|
- **Live reload** — browser auto-refreshes during development
|
|
38
78
|
- **Inline testing** — decorator-based test cases with `@tests`
|
|
39
|
-
- **Localization** — i18n via gettext (English, French, Afrikaans)
|
|
79
|
+
- **Localization** — i18n via gettext (English, French, Afrikaans, Chinese, Japanese, Spanish)
|
|
40
80
|
|
|
41
81
|
## Install
|
|
42
82
|
|
|
@@ -44,12 +84,26 @@ You've just built your first Tina4 app — zero configuration, zero classes, zer
|
|
|
44
84
|
pip install tina4-python
|
|
45
85
|
```
|
|
46
86
|
|
|
47
|
-
Or with [uv](https://docs.astral.sh/uv/) (recommended
|
|
87
|
+
Or with [uv](https://docs.astral.sh/uv/) (recommended):
|
|
48
88
|
|
|
49
89
|
```bash
|
|
50
90
|
uv add tina4-python
|
|
51
91
|
```
|
|
52
92
|
|
|
93
|
+
### Optional extras
|
|
94
|
+
|
|
95
|
+
Install only the database driver you need:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install tina4-python[postgres] # PostgreSQL (psycopg2-binary)
|
|
99
|
+
pip install tina4-python[mysql] # MySQL / MariaDB (mysql-connector-python)
|
|
100
|
+
pip install tina4-python[mssql] # Microsoft SQL Server (pymssql)
|
|
101
|
+
pip install tina4-python[firebird] # Firebird (firebird-driver)
|
|
102
|
+
pip install tina4-python[mongo] # MongoDB (pymongo)
|
|
103
|
+
pip install tina4-python[all-db] # all of the above
|
|
104
|
+
pip install tina4-python[dev-reload] # hot-patching via jurigged
|
|
105
|
+
```
|
|
106
|
+
|
|
53
107
|
## Routing
|
|
54
108
|
|
|
55
109
|
Routes live in `src/routes/` and are auto-discovered on startup.
|
|
@@ -64,7 +118,7 @@ async def get_hello(request, response):
|
|
|
64
118
|
|
|
65
119
|
@get("/hello/{name}")
|
|
66
120
|
async def get_hello_name(name, request, response):
|
|
67
|
-
return response(f"Hello, {name}")
|
|
121
|
+
return response(f"Hello, {name}!")
|
|
68
122
|
|
|
69
123
|
@get("/hello/json")
|
|
70
124
|
async def get_hello_json(request, response):
|
|
@@ -73,10 +127,6 @@ async def get_hello_json(request, response):
|
|
|
73
127
|
@get("/hello/template")
|
|
74
128
|
async def get_hello_template(request, response):
|
|
75
129
|
return response.render("index.twig", {"data": request.params})
|
|
76
|
-
|
|
77
|
-
@get("/hello/redirect")
|
|
78
|
-
async def get_hello_redirect(request, response):
|
|
79
|
-
return response.redirect("/hello/world")
|
|
80
130
|
```
|
|
81
131
|
|
|
82
132
|
## ORM
|
|
@@ -99,11 +149,13 @@ User({"name": "Alice", "email": "alice@example.com"}).save()
|
|
|
99
149
|
from tina4_python.Database import Database
|
|
100
150
|
|
|
101
151
|
db = Database("sqlite3:app.db")
|
|
152
|
+
db = Database("psycopg2:localhost/5432:mydb", "user", "password") # PostgreSQL
|
|
153
|
+
db = Database("mysql.connector:localhost/3306:mydb", "user", "password") # MySQL
|
|
154
|
+
db = Database("pymongo:localhost/27017:mydb") # MongoDB
|
|
155
|
+
|
|
102
156
|
result = db.fetch("SELECT * FROM users WHERE age > ?", [18])
|
|
103
157
|
```
|
|
104
158
|
|
|
105
|
-
Works with SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, and Firebird.
|
|
106
|
-
|
|
107
159
|
## Migrations
|
|
108
160
|
|
|
109
161
|
```bash
|
|
@@ -112,6 +164,48 @@ tina4 migrate:create "create users table"
|
|
|
112
164
|
tina4 migrate
|
|
113
165
|
```
|
|
114
166
|
|
|
167
|
+
## Data Seeder
|
|
168
|
+
|
|
169
|
+
Generate fake data for development and testing:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from tina4_python.Seeder import FakeData, seed_orm, seed_table
|
|
173
|
+
|
|
174
|
+
fake = FakeData()
|
|
175
|
+
fake.name() # "Alice Johnson"
|
|
176
|
+
fake.email() # "alice.johnson@example.com"
|
|
177
|
+
fake.phone() # "+27 82 123 4567"
|
|
178
|
+
|
|
179
|
+
# Seed an ORM model
|
|
180
|
+
seed_orm(User, count=50)
|
|
181
|
+
|
|
182
|
+
# Seed a raw table
|
|
183
|
+
seed_table(db, "products", columns, count=100)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
CLI:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
tina4 seed # run all files in src/seeds/
|
|
190
|
+
tina4 seed:create "initial users" # scaffold a new seed file
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## CRUD Scaffolding
|
|
194
|
+
|
|
195
|
+
Generate a searchable, paginated admin UI with one call:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from tina4_python.CRUD import CRUD
|
|
199
|
+
|
|
200
|
+
@get("/admin/users")
|
|
201
|
+
async def admin_users(request, response):
|
|
202
|
+
return response(CRUD.to_crud(request, {
|
|
203
|
+
"sql": "SELECT id, name, email FROM users",
|
|
204
|
+
"title": "User Management",
|
|
205
|
+
"primary_key": "id",
|
|
206
|
+
}))
|
|
207
|
+
```
|
|
208
|
+
|
|
115
209
|
## Sessions
|
|
116
210
|
|
|
117
211
|
Built-in session management with pluggable backends:
|
|
@@ -124,8 +218,26 @@ Built-in session management with pluggable backends:
|
|
|
124
218
|
| `SessionMongoHandler` | MongoDB | `pymongo` |
|
|
125
219
|
|
|
126
220
|
```python
|
|
127
|
-
request.session.set("
|
|
128
|
-
|
|
221
|
+
request.session.set("user_id", 42)
|
|
222
|
+
user_id = request.session.get("user_id")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## JWT Authentication
|
|
226
|
+
|
|
227
|
+
Tokens are signed with HS256 using your `SECRET` env var. Set it in `.env`:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
SECRET=your-strong-random-secret-32-chars-min
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from tina4_python import tina4_auth
|
|
235
|
+
|
|
236
|
+
token = tina4_auth.get_token({"user_id": 42})
|
|
237
|
+
is_valid = tina4_auth.valid(token)
|
|
238
|
+
payload = tina4_auth.get_payload(token)
|
|
239
|
+
hashed = tina4_auth.hash_password("mypassword")
|
|
240
|
+
ok = tina4_auth.check_password(hashed, "mypassword")
|
|
129
241
|
```
|
|
130
242
|
|
|
131
243
|
## Queues
|
|
@@ -135,10 +247,25 @@ Background processing with litequeue (default), RabbitMQ, Kafka, or MongoDB.
|
|
|
135
247
|
```python
|
|
136
248
|
from tina4_python.Queue import Queue, Producer, Consumer
|
|
137
249
|
|
|
250
|
+
# Enqueue from a route
|
|
138
251
|
Producer(Queue(topic="emails")).produce({"to": "alice@example.com"})
|
|
139
252
|
|
|
253
|
+
# Process in a worker
|
|
140
254
|
for msg in Consumer(Queue(topic="emails")).messages():
|
|
141
|
-
|
|
255
|
+
send_email(msg.data)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## GraphQL
|
|
259
|
+
|
|
260
|
+
Zero-dependency GraphQL engine with ORM auto-schema:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from tina4_python.GraphQL import GraphQL
|
|
264
|
+
|
|
265
|
+
gql = GraphQL()
|
|
266
|
+
gql.schema.from_orm(User) # auto-generates type, queries, and mutations
|
|
267
|
+
gql.schema.from_orm(Product)
|
|
268
|
+
gql.register_route("/graphql") # GET = GraphiQL IDE, POST = queries
|
|
142
269
|
```
|
|
143
270
|
|
|
144
271
|
## Middleware
|
|
@@ -179,25 +306,30 @@ async def users(request, response):
|
|
|
179
306
|
from tina4_python import Api
|
|
180
307
|
|
|
181
308
|
api = Api("https://api.example.com", auth_header="Bearer xyz")
|
|
182
|
-
result = api.
|
|
309
|
+
result = api.send_request("/users/42")
|
|
183
310
|
```
|
|
184
311
|
|
|
185
312
|
## WSDL / SOAP
|
|
186
313
|
|
|
187
314
|
```python
|
|
188
315
|
from tina4_python.WSDL import WSDL, wsdl_operation
|
|
316
|
+
from tina4_python import get, post
|
|
189
317
|
|
|
190
318
|
class Calculator(WSDL):
|
|
191
|
-
|
|
192
|
-
|
|
319
|
+
@wsdl_operation({"Result": int})
|
|
193
320
|
def Add(self, a: int, b: int):
|
|
194
321
|
return {"Result": a + b}
|
|
322
|
+
|
|
323
|
+
@get("/calculator")
|
|
324
|
+
@post("/calculator")
|
|
325
|
+
async def calculator(request, response):
|
|
326
|
+
return response(Calculator(request).handle())
|
|
195
327
|
```
|
|
196
328
|
|
|
197
|
-
## Testing
|
|
329
|
+
## Inline Testing
|
|
198
330
|
|
|
199
331
|
```python
|
|
200
|
-
from tina4_python import tests
|
|
332
|
+
from tina4_python import tests, assert_equal, assert_raises
|
|
201
333
|
|
|
202
334
|
@tests(
|
|
203
335
|
assert_equal((7, 7), 1),
|
|
@@ -209,14 +341,14 @@ def divide(a: int, b: int) -> float:
|
|
|
209
341
|
return a / b
|
|
210
342
|
```
|
|
211
343
|
|
|
212
|
-
Run with `tina4 test` or `uv run pytest
|
|
344
|
+
Run with `tina4 test` or `uv run pytest`.
|
|
213
345
|
|
|
214
346
|
## Environment
|
|
215
347
|
|
|
216
348
|
Key `.env` settings:
|
|
217
349
|
|
|
218
350
|
```bash
|
|
219
|
-
SECRET=your-jwt-secret
|
|
351
|
+
SECRET=your-jwt-secret-32-chars-min
|
|
220
352
|
API_KEY=your-api-key
|
|
221
353
|
DATABASE_NAME=sqlite3:app.db
|
|
222
354
|
TINA4_DEBUG_LEVEL=ALL
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tina4-python
|
|
3
|
-
Version: 0.2.201
|
|
4
|
-
Summary: Tina4Python - This is not another framework for Python
|
|
5
|
-
Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
|
|
6
|
-
Requires-Python: <4.0,>=3.12
|
|
7
|
-
Requires-Dist: asyncer>=0.0.8
|
|
8
|
-
Requires-Dist: bcrypt<5.0.0,>=4.2.1
|
|
9
|
-
Requires-Dist: cryptography<45.0.0,>=44.0.0
|
|
10
|
-
Requires-Dist: hypercorn>=0.18.0
|
|
11
|
-
Requires-Dist: jinja2<4.0.0,>=3.1.5
|
|
12
|
-
Requires-Dist: jurigged>=0.6.0
|
|
13
|
-
Requires-Dist: libsass<0.24.0,>=0.23.0
|
|
14
|
-
Requires-Dist: litequeue<0.10,>=0.9
|
|
15
|
-
Requires-Dist: pyjwt<3.0.0,>=2.10.1
|
|
16
|
-
Requires-Dist: python-dotenv<2.0.0,>=1.0.1
|
|
17
|
-
Requires-Dist: requests>=2.32.5
|
|
18
|
-
Requires-Dist: simple-websocket<2.0.0,>=1.1.0
|
|
19
|
-
Requires-Dist: watchdog<7.0.0,>=6.0.0
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
|
|
22
1
|
# Tina4 Python — This is not a framework
|
|
23
2
|
|
|
24
3
|
Laravel joy. Python speed. 10x less code.
|
|
@@ -42,13 +21,16 @@ You've just built your first Tina4 app — zero configuration, zero classes, zer
|
|
|
42
21
|
- **Full async** — every route handler is `async` by default
|
|
43
22
|
- **Routing** — decorator-based with path parameters, type hints, and auto-discovery
|
|
44
23
|
- **Twig/Jinja2 templates** — with inheritance, partials, custom filters, and globals
|
|
24
|
+
- **tina4-css** — lightweight CSS framework (~24 KB) ships built-in, Bootstrap-compatible class names
|
|
45
25
|
- **ORM** — define models with typed fields, save/load/select/delete with one line
|
|
46
|
-
- **Database** — SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, Firebird
|
|
26
|
+
- **Database** — SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, Firebird, MongoDB
|
|
47
27
|
- **Migrations** — versioned SQL files, CLI scaffolding
|
|
28
|
+
- **Data seeder** — zero-dependency fake data generation with ORM and table support
|
|
48
29
|
- **Sessions** — file, Redis, Valkey, or MongoDB backends
|
|
49
|
-
- **JWT authentication** —
|
|
30
|
+
- **JWT authentication** — HS256 tokens signed with your `SECRET` env var, form tokens
|
|
50
31
|
- **Swagger/OpenAPI** — auto-generated docs at `/swagger`
|
|
51
|
-
- **CRUD scaffolding** — instant admin UI with one line of code
|
|
32
|
+
- **CRUD scaffolding** — instant searchable admin UI with one line of code
|
|
33
|
+
- **GraphQL** — zero-dependency engine with ORM auto-schema and GraphiQL IDE
|
|
52
34
|
- **Middleware** — before/after hooks per route or globally
|
|
53
35
|
- **Queues** — background processing with litequeue, RabbitMQ, Kafka, or MongoDB
|
|
54
36
|
- **WebSockets** — built-in support via `simple-websocket`
|
|
@@ -57,7 +39,7 @@ You've just built your first Tina4 app — zero configuration, zero classes, zer
|
|
|
57
39
|
- **SCSS compilation** — auto-compiled to CSS on save
|
|
58
40
|
- **Live reload** — browser auto-refreshes during development
|
|
59
41
|
- **Inline testing** — decorator-based test cases with `@tests`
|
|
60
|
-
- **Localization** — i18n via gettext (English, French, Afrikaans)
|
|
42
|
+
- **Localization** — i18n via gettext (English, French, Afrikaans, Chinese, Japanese, Spanish)
|
|
61
43
|
|
|
62
44
|
## Install
|
|
63
45
|
|
|
@@ -65,12 +47,26 @@ You've just built your first Tina4 app — zero configuration, zero classes, zer
|
|
|
65
47
|
pip install tina4-python
|
|
66
48
|
```
|
|
67
49
|
|
|
68
|
-
Or with [uv](https://docs.astral.sh/uv/) (recommended
|
|
50
|
+
Or with [uv](https://docs.astral.sh/uv/) (recommended):
|
|
69
51
|
|
|
70
52
|
```bash
|
|
71
53
|
uv add tina4-python
|
|
72
54
|
```
|
|
73
55
|
|
|
56
|
+
### Optional extras
|
|
57
|
+
|
|
58
|
+
Install only the database driver you need:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install tina4-python[postgres] # PostgreSQL (psycopg2-binary)
|
|
62
|
+
pip install tina4-python[mysql] # MySQL / MariaDB (mysql-connector-python)
|
|
63
|
+
pip install tina4-python[mssql] # Microsoft SQL Server (pymssql)
|
|
64
|
+
pip install tina4-python[firebird] # Firebird (firebird-driver)
|
|
65
|
+
pip install tina4-python[mongo] # MongoDB (pymongo)
|
|
66
|
+
pip install tina4-python[all-db] # all of the above
|
|
67
|
+
pip install tina4-python[dev-reload] # hot-patching via jurigged
|
|
68
|
+
```
|
|
69
|
+
|
|
74
70
|
## Routing
|
|
75
71
|
|
|
76
72
|
Routes live in `src/routes/` and are auto-discovered on startup.
|
|
@@ -85,7 +81,7 @@ async def get_hello(request, response):
|
|
|
85
81
|
|
|
86
82
|
@get("/hello/{name}")
|
|
87
83
|
async def get_hello_name(name, request, response):
|
|
88
|
-
return response(f"Hello, {name}")
|
|
84
|
+
return response(f"Hello, {name}!")
|
|
89
85
|
|
|
90
86
|
@get("/hello/json")
|
|
91
87
|
async def get_hello_json(request, response):
|
|
@@ -94,10 +90,6 @@ async def get_hello_json(request, response):
|
|
|
94
90
|
@get("/hello/template")
|
|
95
91
|
async def get_hello_template(request, response):
|
|
96
92
|
return response.render("index.twig", {"data": request.params})
|
|
97
|
-
|
|
98
|
-
@get("/hello/redirect")
|
|
99
|
-
async def get_hello_redirect(request, response):
|
|
100
|
-
return response.redirect("/hello/world")
|
|
101
93
|
```
|
|
102
94
|
|
|
103
95
|
## ORM
|
|
@@ -120,11 +112,13 @@ User({"name": "Alice", "email": "alice@example.com"}).save()
|
|
|
120
112
|
from tina4_python.Database import Database
|
|
121
113
|
|
|
122
114
|
db = Database("sqlite3:app.db")
|
|
115
|
+
db = Database("psycopg2:localhost/5432:mydb", "user", "password") # PostgreSQL
|
|
116
|
+
db = Database("mysql.connector:localhost/3306:mydb", "user", "password") # MySQL
|
|
117
|
+
db = Database("pymongo:localhost/27017:mydb") # MongoDB
|
|
118
|
+
|
|
123
119
|
result = db.fetch("SELECT * FROM users WHERE age > ?", [18])
|
|
124
120
|
```
|
|
125
121
|
|
|
126
|
-
Works with SQLite, PostgreSQL, MySQL, MariaDB, MSSQL, and Firebird.
|
|
127
|
-
|
|
128
122
|
## Migrations
|
|
129
123
|
|
|
130
124
|
```bash
|
|
@@ -133,6 +127,48 @@ tina4 migrate:create "create users table"
|
|
|
133
127
|
tina4 migrate
|
|
134
128
|
```
|
|
135
129
|
|
|
130
|
+
## Data Seeder
|
|
131
|
+
|
|
132
|
+
Generate fake data for development and testing:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from tina4_python.Seeder import FakeData, seed_orm, seed_table
|
|
136
|
+
|
|
137
|
+
fake = FakeData()
|
|
138
|
+
fake.name() # "Alice Johnson"
|
|
139
|
+
fake.email() # "alice.johnson@example.com"
|
|
140
|
+
fake.phone() # "+27 82 123 4567"
|
|
141
|
+
|
|
142
|
+
# Seed an ORM model
|
|
143
|
+
seed_orm(User, count=50)
|
|
144
|
+
|
|
145
|
+
# Seed a raw table
|
|
146
|
+
seed_table(db, "products", columns, count=100)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
CLI:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
tina4 seed # run all files in src/seeds/
|
|
153
|
+
tina4 seed:create "initial users" # scaffold a new seed file
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## CRUD Scaffolding
|
|
157
|
+
|
|
158
|
+
Generate a searchable, paginated admin UI with one call:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from tina4_python.CRUD import CRUD
|
|
162
|
+
|
|
163
|
+
@get("/admin/users")
|
|
164
|
+
async def admin_users(request, response):
|
|
165
|
+
return response(CRUD.to_crud(request, {
|
|
166
|
+
"sql": "SELECT id, name, email FROM users",
|
|
167
|
+
"title": "User Management",
|
|
168
|
+
"primary_key": "id",
|
|
169
|
+
}))
|
|
170
|
+
```
|
|
171
|
+
|
|
136
172
|
## Sessions
|
|
137
173
|
|
|
138
174
|
Built-in session management with pluggable backends:
|
|
@@ -145,8 +181,26 @@ Built-in session management with pluggable backends:
|
|
|
145
181
|
| `SessionMongoHandler` | MongoDB | `pymongo` |
|
|
146
182
|
|
|
147
183
|
```python
|
|
148
|
-
request.session.set("
|
|
149
|
-
|
|
184
|
+
request.session.set("user_id", 42)
|
|
185
|
+
user_id = request.session.get("user_id")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## JWT Authentication
|
|
189
|
+
|
|
190
|
+
Tokens are signed with HS256 using your `SECRET` env var. Set it in `.env`:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
SECRET=your-strong-random-secret-32-chars-min
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from tina4_python import tina4_auth
|
|
198
|
+
|
|
199
|
+
token = tina4_auth.get_token({"user_id": 42})
|
|
200
|
+
is_valid = tina4_auth.valid(token)
|
|
201
|
+
payload = tina4_auth.get_payload(token)
|
|
202
|
+
hashed = tina4_auth.hash_password("mypassword")
|
|
203
|
+
ok = tina4_auth.check_password(hashed, "mypassword")
|
|
150
204
|
```
|
|
151
205
|
|
|
152
206
|
## Queues
|
|
@@ -156,10 +210,25 @@ Background processing with litequeue (default), RabbitMQ, Kafka, or MongoDB.
|
|
|
156
210
|
```python
|
|
157
211
|
from tina4_python.Queue import Queue, Producer, Consumer
|
|
158
212
|
|
|
213
|
+
# Enqueue from a route
|
|
159
214
|
Producer(Queue(topic="emails")).produce({"to": "alice@example.com"})
|
|
160
215
|
|
|
216
|
+
# Process in a worker
|
|
161
217
|
for msg in Consumer(Queue(topic="emails")).messages():
|
|
162
|
-
|
|
218
|
+
send_email(msg.data)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## GraphQL
|
|
222
|
+
|
|
223
|
+
Zero-dependency GraphQL engine with ORM auto-schema:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from tina4_python.GraphQL import GraphQL
|
|
227
|
+
|
|
228
|
+
gql = GraphQL()
|
|
229
|
+
gql.schema.from_orm(User) # auto-generates type, queries, and mutations
|
|
230
|
+
gql.schema.from_orm(Product)
|
|
231
|
+
gql.register_route("/graphql") # GET = GraphiQL IDE, POST = queries
|
|
163
232
|
```
|
|
164
233
|
|
|
165
234
|
## Middleware
|
|
@@ -200,25 +269,30 @@ async def users(request, response):
|
|
|
200
269
|
from tina4_python import Api
|
|
201
270
|
|
|
202
271
|
api = Api("https://api.example.com", auth_header="Bearer xyz")
|
|
203
|
-
result = api.
|
|
272
|
+
result = api.send_request("/users/42")
|
|
204
273
|
```
|
|
205
274
|
|
|
206
275
|
## WSDL / SOAP
|
|
207
276
|
|
|
208
277
|
```python
|
|
209
278
|
from tina4_python.WSDL import WSDL, wsdl_operation
|
|
279
|
+
from tina4_python import get, post
|
|
210
280
|
|
|
211
281
|
class Calculator(WSDL):
|
|
212
|
-
|
|
213
|
-
|
|
282
|
+
@wsdl_operation({"Result": int})
|
|
214
283
|
def Add(self, a: int, b: int):
|
|
215
284
|
return {"Result": a + b}
|
|
285
|
+
|
|
286
|
+
@get("/calculator")
|
|
287
|
+
@post("/calculator")
|
|
288
|
+
async def calculator(request, response):
|
|
289
|
+
return response(Calculator(request).handle())
|
|
216
290
|
```
|
|
217
291
|
|
|
218
|
-
## Testing
|
|
292
|
+
## Inline Testing
|
|
219
293
|
|
|
220
294
|
```python
|
|
221
|
-
from tina4_python import tests
|
|
295
|
+
from tina4_python import tests, assert_equal, assert_raises
|
|
222
296
|
|
|
223
297
|
@tests(
|
|
224
298
|
assert_equal((7, 7), 1),
|
|
@@ -230,14 +304,14 @@ def divide(a: int, b: int) -> float:
|
|
|
230
304
|
return a / b
|
|
231
305
|
```
|
|
232
306
|
|
|
233
|
-
Run with `tina4 test` or `uv run pytest
|
|
307
|
+
Run with `tina4 test` or `uv run pytest`.
|
|
234
308
|
|
|
235
309
|
## Environment
|
|
236
310
|
|
|
237
311
|
Key `.env` settings:
|
|
238
312
|
|
|
239
313
|
```bash
|
|
240
|
-
SECRET=your-jwt-secret
|
|
314
|
+
SECRET=your-jwt-secret-32-chars-min
|
|
241
315
|
API_KEY=your-api-key
|
|
242
316
|
DATABASE_NAME=sqlite3:app.db
|
|
243
317
|
TINA4_DEBUG_LEVEL=ALL
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tina4-python"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.203"
|
|
4
4
|
description = "Tina4Python - This is not another framework for Python"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
|
|
@@ -11,8 +11,7 @@ dependencies = [
|
|
|
11
11
|
"jinja2>=3.1.5,<4.0.0",
|
|
12
12
|
"libsass (>=0.23.0,<0.24.0)",
|
|
13
13
|
"python-dotenv (>=1.0.1,<2.0.0)",
|
|
14
|
-
"pyjwt
|
|
15
|
-
"cryptography (>=44.0.0,<45.0.0)",
|
|
14
|
+
"pyjwt>=2.10.1,<3.0.0",
|
|
16
15
|
"watchdog (>=6.0.0,<7.0.0)",
|
|
17
16
|
"bcrypt (>=4.2.1,<5.0.0)",
|
|
18
17
|
"litequeue (>=0.9,<0.10)",
|
|
@@ -20,11 +19,26 @@ dependencies = [
|
|
|
20
19
|
"asyncer>=0.0.8",
|
|
21
20
|
"hypercorn>=0.18.0",
|
|
22
21
|
"requests>=2.32.5",
|
|
23
|
-
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev-reload = ["jurigged>=0.6.0"]
|
|
26
|
+
postgres = ["psycopg2-binary>=2.9.10"]
|
|
27
|
+
mysql = ["mysql-connector-python>=9.3.0"]
|
|
28
|
+
mssql = ["pymssql>=2.3.0"]
|
|
29
|
+
firebird = ["firebird-driver>=1.10.0"]
|
|
30
|
+
mongo = ["pymongo>=4.0.0"]
|
|
31
|
+
all-db = [
|
|
32
|
+
"psycopg2-binary>=2.9.10",
|
|
33
|
+
"mysql-connector-python>=9.3.0",
|
|
34
|
+
"pymssql>=2.3.0",
|
|
35
|
+
"firebird-driver>=1.10.0",
|
|
36
|
+
"pymongo>=4.0.0",
|
|
24
37
|
]
|
|
25
38
|
|
|
26
39
|
[dependency-groups]
|
|
27
40
|
dev = [
|
|
41
|
+
"jurigged>=0.6.0",
|
|
28
42
|
"flake8>=7.2.0",
|
|
29
43
|
"mkdocs>=1.6.1",
|
|
30
44
|
"mysql-connector-python>=9.3.0",
|