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.
Files changed (88) hide show
  1. {tina4_python-0.2.202 → tina4_python-0.2.204}/PKG-INFO +119 -25
  2. {tina4_python-0.2.202 → tina4_python-0.2.204}/README.md +117 -22
  3. {tina4_python-0.2.202 → tina4_python-0.2.204}/pyproject.toml +2 -4
  4. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/DevReload.py +14 -3
  5. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Router.py +270 -77
  6. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Webserver.py +1 -1
  7. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/__init__.py +90 -63
  8. {tina4_python-0.2.202 → tina4_python-0.2.204}/.gitignore +0 -0
  9. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Api.py +0 -0
  10. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Auth.py +0 -0
  11. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/CLAUDE.md +0 -0
  12. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/CRUD.py +0 -0
  13. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Constant.py +0 -0
  14. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Database.py +0 -0
  15. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/DatabaseResult.py +0 -0
  16. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/DatabaseTypes.py +0 -0
  17. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Debug.py +0 -0
  18. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Env.py +0 -0
  19. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/FieldTypes.py +0 -0
  20. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/GraphQL.py +0 -0
  21. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/HtmlElement.py +0 -0
  22. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Localization.py +0 -0
  23. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Messages.py +0 -0
  24. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/MiddleWare.py +0 -0
  25. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Migration.py +0 -0
  26. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/ORM.py +0 -0
  27. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Queue.py +0 -0
  28. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Request.py +0 -0
  29. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Response.py +0 -0
  30. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/SQLToMongo.py +0 -0
  31. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Seeder.py +0 -0
  32. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Session.py +0 -0
  33. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/ShellColors.py +0 -0
  34. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Swagger.py +0 -0
  35. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Template.py +0 -0
  36. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Testing.py +0 -0
  37. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/WSDL.py +0 -0
  38. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/Websocket.py +0 -0
  39. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/cli.py +0 -0
  40. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/messages.pot +0 -0
  41. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/css/readme.md +0 -0
  42. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/css/tina4.css +0 -0
  43. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/css/tina4.min.css +0 -0
  44. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/favicon.ico +0 -0
  45. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/images/403.png +0 -0
  46. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/images/404.png +0 -0
  47. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/images/500.png +0 -0
  48. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/images/logo.png +0 -0
  49. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/images/readme.md +0 -0
  50. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/js/readme.md +0 -0
  51. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  52. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/js/tina4.js +0 -0
  53. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/js/tina4helper.js +0 -0
  54. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/swagger/index.html +0 -0
  55. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  56. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  57. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_badges.scss +0 -0
  58. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  59. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_cards.scss +0 -0
  60. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_forms.scss +0 -0
  61. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_grid.scss +0 -0
  62. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_modals.scss +0 -0
  63. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_nav.scss +0 -0
  64. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_reset.scss +0 -0
  65. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_tables.scss +0 -0
  66. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_typography.scss +0 -0
  67. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  68. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/_variables.scss +0 -0
  69. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/base.scss +0 -0
  70. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/colors.scss +0 -0
  71. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/scss/tina4css/tina4.scss +0 -0
  72. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/templates/components/crud.twig +0 -0
  73. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/templates/errors/403.twig +0 -0
  74. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/templates/errors/404.twig +0 -0
  75. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/templates/errors/500.twig +0 -0
  76. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/templates/readme.md +0 -0
  77. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  78. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  79. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  80. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  81. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  82. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  83. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  84. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  85. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  86. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  87. {tina4_python-0.2.202 → tina4_python-0.2.204}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  88. {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.202
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** — auto-generated RSA keys, bearer tokens, form tokens
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 for dependency management):
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("name", "Joe")
165
- name = request.session.get("name")
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
- print(msg.data)
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.get("/users/42")
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
- SERVICE_URL = "http://localhost:7145/calculator"
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 --verbose`.
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** — auto-generated RSA keys, bearer tokens, form tokens
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 for dependency management):
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("name", "Joe")
128
- name = request.session.get("name")
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
- print(msg.data)
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.get("/users/42")
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
- SERVICE_URL = "http://localhost:7145/calculator"
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 --verbose`.
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.202"
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
- from watchdog.observers import Observer
41
- from watchdog.events import PatternMatchingEventHandler, FileSystemEvent
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")