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