tina4-python 0.2.168__tar.gz → 0.2.170__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 (56) hide show
  1. {tina4_python-0.2.168 → tina4_python-0.2.170}/PKG-INFO +1 -1
  2. {tina4_python-0.2.168 → tina4_python-0.2.170}/pyproject.toml +65 -65
  3. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/CRUD.py +42 -9
  4. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/FieldTypes.py +16 -9
  5. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/ORM.py +5 -4
  6. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/__init__.py +5 -1
  7. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/templates/components/crud.twig +31 -4
  8. {tina4_python-0.2.168 → tina4_python-0.2.170}/.gitignore +0 -0
  9. {tina4_python-0.2.168 → tina4_python-0.2.170}/README.md +0 -0
  10. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Api.py +0 -0
  11. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Auth.py +0 -0
  12. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Constant.py +0 -0
  13. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Database.py +0 -0
  14. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/DatabaseResult.py +0 -0
  15. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/DatabaseTypes.py +0 -0
  16. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Debug.py +0 -0
  17. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Env.py +0 -0
  18. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/HtmlElement.py +0 -0
  19. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Localization.py +0 -0
  20. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Messages.py +0 -0
  21. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/MiddleWare.py +0 -0
  22. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Migration.py +0 -0
  23. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Queue.py +0 -0
  24. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Request.py +0 -0
  25. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Response.py +0 -0
  26. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Router.py +0 -0
  27. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Session.py +0 -0
  28. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/ShellColors.py +0 -0
  29. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Swagger.py +0 -0
  30. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Template.py +0 -0
  31. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Testing.py +0 -0
  32. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/WSDL.py +0 -0
  33. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Webserver.py +0 -0
  34. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/Websocket.py +0 -0
  35. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/cli.py +0 -0
  36. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/messages.pot +0 -0
  37. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/css/readme.md +0 -0
  38. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/favicon.ico +0 -0
  39. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/images/403.png +0 -0
  40. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/images/404.png +0 -0
  41. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/images/500.png +0 -0
  42. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/images/logo.png +0 -0
  43. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/images/readme.md +0 -0
  44. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/js/readme.md +0 -0
  45. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  46. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/js/tina4helper.js +0 -0
  47. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/swagger/index.html +0 -0
  48. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  49. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/templates/errors/403.twig +0 -0
  50. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/templates/errors/404.twig +0 -0
  51. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/templates/errors/500.twig +0 -0
  52. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/templates/readme.md +0 -0
  53. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  54. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  55. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  56. {tina4_python-0.2.168 → tina4_python-0.2.170}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 0.2.168
3
+ Version: 0.2.170
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
@@ -1,65 +1,65 @@
1
- [project]
2
- name = "tina4-python"
3
- version = "0.2.168"
4
- description = "Tina4Python - This is not another framework for Python"
5
- authors = [
6
- {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
7
- ]
8
- readme = "README.md"
9
- requires-python = ">=3.12,<4.0"
10
- dependencies = [
11
- "jinja2>=3.1.5,<4.0.0",
12
- "libsass (>=0.23.0,<0.24.0)",
13
- "python-dotenv (>=1.0.1,<2.0.0)",
14
- "pyjwt (>=2.10.1,<3.0.0)",
15
- "cryptography (>=44.0.0,<45.0.0)",
16
- "watchdog (>=6.0.0,<7.0.0)",
17
- "bcrypt (>=4.2.1,<5.0.0)",
18
- "litequeue (>=0.9,<0.10)",
19
- "simple-websocket (>=1.1.0,<2.0.0)",
20
- "asyncer>=0.0.8",
21
- "hypercorn>=0.18.0",
22
- "requests>=2.32.5",
23
- ]
24
-
25
- [dependency-groups]
26
- dev = [
27
- "flake8>=7.2.0",
28
- "jurigged>=0.6.0",
29
- "mkdocs>=1.6.1",
30
- "mysql-connector-python>=9.3.0",
31
- "psycopg2-binary>=2.9.10",
32
- "pydoc-markdown>=4.8.2",
33
- "pytest>=8.3.5",
34
- "pytest-asyncio>=1.3.0",
35
- "python-keycloak>=5.8.1",
36
- "ruff>=0.11.9",
37
- "safety>=3.5.0",
38
- ]
39
-
40
- [build-system]
41
- requires = ["hatchling"]
42
- build-backend = "hatchling.build"
43
-
44
- [tool.hatch.build]
45
- include = [
46
- "tina4_python/**/*"
47
- ]
48
-
49
- [project.scripts]
50
- tina4 = "tina4_python.cli:main"
51
-
52
- [[tool.pydoc-markdown.loaders]]
53
- type = "python"
54
- search_path = [ "./tina4_python" ]
55
-
56
- [tool.pydoc-markdown.renderer]
57
- type = "mkdocs"
58
- site_name= "Tina4Python"
59
-
60
- [[tool.pydoc-markdown.renderer.pages]]
61
- title = "API Documentation"
62
- name = "index"
63
-
64
- contents = [ "*" ]
65
-
1
+ [project]
2
+ name = "tina4-python"
3
+ version = "0.2.170"
4
+ description = "Tina4Python - This is not another framework for Python"
5
+ authors = [
6
+ {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.12,<4.0"
10
+ dependencies = [
11
+ "jinja2>=3.1.5,<4.0.0",
12
+ "libsass (>=0.23.0,<0.24.0)",
13
+ "python-dotenv (>=1.0.1,<2.0.0)",
14
+ "pyjwt (>=2.10.1,<3.0.0)",
15
+ "cryptography (>=44.0.0,<45.0.0)",
16
+ "watchdog (>=6.0.0,<7.0.0)",
17
+ "bcrypt (>=4.2.1,<5.0.0)",
18
+ "litequeue (>=0.9,<0.10)",
19
+ "simple-websocket (>=1.1.0,<2.0.0)",
20
+ "asyncer>=0.0.8",
21
+ "hypercorn>=0.18.0",
22
+ "requests>=2.32.5",
23
+ ]
24
+
25
+ [dependency-groups]
26
+ dev = [
27
+ "flake8>=7.2.0",
28
+ "jurigged>=0.6.0",
29
+ "mkdocs>=1.6.1",
30
+ "mysql-connector-python>=9.3.0",
31
+ "psycopg2-binary>=2.9.10",
32
+ "pydoc-markdown>=4.8.2",
33
+ "pytest>=8.3.5",
34
+ "pytest-asyncio>=1.3.0",
35
+ "python-keycloak>=5.8.1",
36
+ "ruff>=0.11.9",
37
+ "safety>=3.5.0",
38
+ ]
39
+
40
+ [build-system]
41
+ requires = ["hatchling"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.build]
45
+ include = [
46
+ "tina4_python/**/*"
47
+ ]
48
+
49
+ [project.scripts]
50
+ tina4 = "tina4_python.cli:main"
51
+
52
+ [[tool.pydoc-markdown.loaders]]
53
+ type = "python"
54
+ search_path = [ "./tina4_python" ]
55
+
56
+ [tool.pydoc-markdown.renderer]
57
+ type = "mkdocs"
58
+ site_name= "Tina4Python"
59
+
60
+ [[tool.pydoc-markdown.renderer.pages]]
61
+ title = "API Documentation"
62
+ name = "index"
63
+
64
+ contents = [ "*" ]
65
+
@@ -8,6 +8,7 @@
8
8
  import base64
9
9
  import datetime
10
10
  from decimal import Decimal
11
+ from shlex import join
11
12
 
12
13
  from tina4_python.Debug import Debug
13
14
  import re
@@ -252,6 +253,21 @@ class CRUD:
252
253
 
253
254
  twig_file = self.ensure_crud_template(crud_name + ".twig")
254
255
 
256
+ def parse_request_for_images(request):
257
+ """
258
+ Parses request for images and files
259
+ :param request:
260
+ :return:
261
+ """
262
+ # if there are files in the request the then I assign the file value to the request body
263
+ if request.files:
264
+ for input_name in request.files:
265
+ prefix = ""
266
+ if "image" in request.files[input_name]["content_type"]:
267
+ prefix ="data:"+request.files[input_name]["content_type"]+";base64,"
268
+ request.body[input_name] = prefix+request.files[input_name]["content"]
269
+ return request
270
+
255
271
  async def get_record(request, response):
256
272
  limit = int(request.params.get("limit", options.get("limit", 10)))
257
273
  offset = int(request.params.get("offset", options.get("offset", 0)))
@@ -285,12 +301,21 @@ class CRUD:
285
301
 
286
302
  async def post_record(request, response):
287
303
  Debug.info("CRUD CREATE", table_name, request.body)
304
+
305
+ request = parse_request_for_images(request)
306
+
288
307
  self.dba.insert(table_name, request.body, primary_key=options["primary_key"])
289
308
  self.dba.commit()
290
309
  return response({"message": f"<script>showMessage('{table_nice_name} Record added');</script>"}, HTTP_OK, APPLICATION_JSON)
291
310
 
292
311
  async def update_record(request, response):
293
312
  Debug.info("CRUD UPDATE", table_name, request.params, request.body)
313
+ # Add the primary key if it is taken out of the form
314
+ if options["primary_key"] not in request.body:
315
+ request.body[options["primary_key"]] = request.params[options["primary_key"]]
316
+
317
+ request = parse_request_for_images(request)
318
+
294
319
  self.dba.update(table_name, request.body, primary_key=options["primary_key"])
295
320
  self.dba.commit()
296
321
  return response({"message": f"<script>showMessage('{table_nice_name} Record updated');</script>", "post": request.body}, HTTP_OK, APPLICATION_JSON)
@@ -311,7 +336,7 @@ class CRUD:
311
336
  fields.append({"name": column, "label": Template.get_nice_label(column)})
312
337
 
313
338
  html = Template.render(twig_file.replace(os.path.join(tina4_python.root_path, "src", "templates"), "").replace("\\", "/"),
314
- {"columns": fields, "records": self.to_array(),
339
+ {"columns": fields, "records": self.to_array(base64_encode=False),
315
340
  "table_name": crud_name,
316
341
  "total_records": self.total_count,
317
342
  "options": options})
@@ -320,7 +345,7 @@ class CRUD:
320
345
  except Exception as e:
321
346
  return "Error rendering CRUD: "+str(e)
322
347
 
323
- def to_array(self, _filter=None):
348
+ def to_array(self, _filter=None, base64_encode = True):
324
349
  """
325
350
  Convert the internal records to a JSON-serializable list of dictionaries.
326
351
 
@@ -334,6 +359,8 @@ class CRUD:
334
359
 
335
360
  Returns:
336
361
  list[dict]: Serializable records.
362
+ :param _filter:
363
+ :param base64_encode: Override default base64 encoding
337
364
  """
338
365
  if self.error is not None:
339
366
  return {"error": self.error}
@@ -348,9 +375,15 @@ class CRUD:
348
375
  elif isinstance(record[key], (datetime.date, datetime.datetime)):
349
376
  json_record[key] = record[key].isoformat()
350
377
  elif isinstance(record[key], memoryview):
351
- json_record[key] = base64.b64encode(record[key].tobytes()).decode('utf-8')
352
- elif isinstance(record[key], bytes):
353
- json_record[key] = base64.b64encode(record[key]).decode('utf-8')
378
+ if base64_encode:
379
+ json_record[key] = base64.b64encode(record[key].tobytes()).decode('utf-8')
380
+ else:
381
+ json_record[key] = record[key].tobytes().decode("utf-8")
382
+ elif isinstance(record[key], bytes) and base64_encode:
383
+ if base64_encode:
384
+ json_record[key] = base64.b64encode(record[key]).decode('utf-8')
385
+ else:
386
+ json_record[key] = record[key].decode("utf-8")
354
387
  else:
355
388
  json_record[key] = record[key]
356
389
 
@@ -363,13 +396,13 @@ class CRUD:
363
396
  else:
364
397
  return []
365
398
 
366
- def to_list(self, _filter=None):
399
+ def to_list(self, _filter=None, base64_encode = True):
367
400
  """Alias of to_array() for readability."""
368
- return self.to_array(_filter)
401
+ return self.to_array(_filter, base64_encode=base64_encode)
369
402
 
370
- def to_json(self, _filter=None):
403
+ def to_json(self, _filter=None, base64_encode = True):
371
404
  """Return records as a JSON encoded string."""
372
- return json.dumps(self.to_array(_filter))
405
+ return json.dumps(self.to_array(_filter, base64_encode=base64_encode))
373
406
 
374
407
  def __iter__(self):
375
408
  """Allow iteration over records (yields results from to_array())."""
@@ -9,6 +9,7 @@ import ast
9
9
  import inspect
10
10
  from datetime import datetime
11
11
  from tina4_python.DatabaseTypes import MSSQL, POSTGRES, FIREBIRD
12
+ from tina4_python import Debug
12
13
 
13
14
  class BaseField:
14
15
  primary_key = False
@@ -83,15 +84,21 @@ class BaseField:
83
84
  self.decimal_places = decimal_places
84
85
 
85
86
  if column_name is None:
86
- frame = inspect.stack()[1]
87
- # Parse python syntax of the assignment line
88
- st = ast.parse(frame.code_context[0].strip())
89
- stmt = st.body[0]
90
- # Assume class being instanced as simple assign statement
91
- assert (isinstance(stmt, ast.Assign))
92
- # Parse the target the class is assigned to
93
- target = stmt.targets[0]
94
- self.column_name = target.id
87
+ try:
88
+ frame = inspect.stack()[1]
89
+ # Parse python syntax of the assignment line
90
+ st = ast.parse(frame.code_context[0].strip())
91
+ stmt = st.body[0]
92
+ # Assume class being instanced as simple assign statement
93
+ assert (isinstance(stmt, ast.Assign))
94
+ # Parse the target the class is assigned to
95
+ target = stmt.targets[0]
96
+ if hasattr(target, 'id'):
97
+ self.column_name = target.id
98
+ else:
99
+ raise Exception("Sorry we can't determine the column name for ORM ")
100
+ except Exception as e:
101
+ Debug.error("Error determining field column for ORM Object", str(e))
95
102
  else:
96
103
  self.column_name = column_name
97
104
 
@@ -5,12 +5,10 @@
5
5
  #
6
6
  # flake8: noqa: E501
7
7
  import base64
8
- from datetime import datetime, date
9
- import ast
8
+ from datetime import date
10
9
  import json
11
10
  import os
12
11
  from tina4_python.Constant import TINA4_LOG_ERROR
13
- from tina4_python.Debug import Debug
14
12
  from tina4_python.FieldTypes import *
15
13
 
16
14
 
@@ -186,7 +184,10 @@ class ORM:
186
184
 
187
185
  data[key] = current_value.value
188
186
  elif isinstance(value, IntegerField):
189
- data[key] = int(current_value)
187
+ try:
188
+ data[key] = int(current_value)
189
+ except Exception as e:
190
+ Debug.error("Could not save", current_value, "to", key)
190
191
  else:
191
192
  data[key] = str(current_value)
192
193
 
@@ -217,7 +217,7 @@ from .Router import get, post, put, patch, delete, middleware, cached, noauth, s
217
217
  from .Testing import tests, assert_equal, assert_raises
218
218
  from .Debug import Debug
219
219
  from .Database import Database
220
- from .ORM import ORM
220
+ from .ORM import ORM, orm
221
221
  from .Api import Api
222
222
  from .Template import template
223
223
  from .Swagger import description, secure, summary, example, example_response, tags, params, describe
@@ -231,10 +231,14 @@ for deco in (get, post, put, patch, delete, middleware, cached, noauth, secured,
231
231
  if deco.__name__ not in builtins.__dict__:
232
232
  builtins.__dict__[deco.__name__] = deco
233
233
 
234
+
235
+
234
236
  builtins.Debug = Debug
235
237
  builtins.Api = Api
236
238
  builtins.Database = Database
237
239
  builtins.ORM = ORM
240
+ builtins.orm = orm
241
+
238
242
 
239
243
  # Auto-import everything from src folders
240
244
  if os.path.exists(root_path + os.sep + "src"):
@@ -59,7 +59,7 @@
59
59
  {# --- Only show image if it's base64-like and long enough --- #}
60
60
  {% if is_image and value|length > 50 and value is string %}
61
61
  {% set src = value %}
62
- {% if value[:10] != 'data:image/' %}
62
+ {% if value[:11] != 'data:image/' %}
63
63
  {% set src = 'data:image/' ~ mime_type ~ ';base64,' ~ value %}
64
64
  {% endif %}
65
65
 
@@ -131,7 +131,7 @@
131
131
  {# --- Only show image if it's base64-like and long enough --- #}
132
132
  {% if is_image %}
133
133
  {% set src = value %}
134
- {% if value[:10] != 'data:image/' %}
134
+ {% if value[:11] != 'data:image/' %}
135
135
  {% set src = 'data:image/' ~ mime_type ~ ';base64,' ~ value %}
136
136
  {% endif %}
137
137
 
@@ -253,7 +253,7 @@
253
253
 
254
254
  html += `
255
255
  <div class="mb-3">
256
- <label class="form-label fw-bold">${label}</label>
256
+ <label class="form-label">${label}</label>
257
257
  <div class="border rounded p-3 bg-light text-center">
258
258
  <img src="${imageSrc}"
259
259
  alt="Current ${label}"
@@ -317,7 +317,33 @@
317
317
  value = '';
318
318
  type = `{{ column.type|default('text') }}`;
319
319
 
320
- html += `<div class="mb-3">
320
+ if (field.indexOf('image') !== -1) {
321
+ let imageSrc = value;
322
+ if (value.length > 0 && !value.startsWith('data:image/')) {
323
+ imageSrc = `data:image/${mimeType};base64,${value}`;
324
+ }
325
+
326
+ html += `
327
+ <div class="mb-3">
328
+ <label class="form-label">${label}</label>
329
+ <div class="border rounded p-3 bg-light text-center">
330
+ <img src="${imageSrc}"
331
+ alt="Current ${label}"
332
+ class="img-thumbnail mb-3"
333
+ style="max-height: 180px; max-width: 100%;">
334
+ <div>
335
+ <input type="file"
336
+ accept="image/*"
337
+ class="form-control form-control-sm"
338
+ id="${field}"
339
+ name="${field}">
340
+ <small class="text-muted">Choose new image (optional)</small>
341
+ </div>
342
+ </div>
343
+ </div>`;
344
+ } else {
345
+
346
+ html += `<div class="mb-3">
321
347
  <label for="${field}" class="form-label">${label}</label>
322
348
  <input type="${type === 'password' ? 'password' : 'text'}"
323
349
  class="form-control"
@@ -326,6 +352,7 @@
326
352
  placeholder="${label}"
327
353
  value='${value}'>
328
354
  </div>`;
355
+ }
329
356
  {% endfor %}
330
357
 
331
358
  modalBody.innerHTML = html;
File without changes