tina4-python 0.2.124__tar.gz → 0.2.125__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 (51) hide show
  1. {tina4_python-0.2.124 → tina4_python-0.2.125}/PKG-INFO +1 -1
  2. {tina4_python-0.2.124 → tina4_python-0.2.125}/pyproject.toml +1 -1
  3. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/CRUD.py +9 -3
  4. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Template.py +48 -75
  5. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/templates/components/crud.twig +9 -39
  6. {tina4_python-0.2.124 → tina4_python-0.2.125}/.gitignore +0 -0
  7. {tina4_python-0.2.124 → tina4_python-0.2.125}/README.md +0 -0
  8. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Auth.py +0 -0
  9. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Constant.py +0 -0
  10. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Database.py +0 -0
  11. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/DatabaseResult.py +0 -0
  12. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/DatabaseTypes.py +0 -0
  13. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Debug.py +0 -0
  14. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Env.py +0 -0
  15. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/HtmlElement.py +0 -0
  16. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Localization.py +0 -0
  17. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Messages.py +0 -0
  18. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/MiddleWare.py +0 -0
  19. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Migration.py +0 -0
  20. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/ORM.py +0 -0
  21. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Queue.py +0 -0
  22. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Request.py +0 -0
  23. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Response.py +0 -0
  24. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Router.py +0 -0
  25. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Session.py +0 -0
  26. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/ShellColors.py +0 -0
  27. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Swagger.py +0 -0
  28. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Webserver.py +0 -0
  29. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/Websocket.py +0 -0
  30. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/__init__.py +0 -0
  31. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/messages.pot +0 -0
  32. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/css/readme.md +0 -0
  33. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/favicon.ico +0 -0
  34. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/images/403.png +0 -0
  35. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/images/404.png +0 -0
  36. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/images/500.png +0 -0
  37. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/images/logo.png +0 -0
  38. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/images/readme.md +0 -0
  39. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/js/readme.md +0 -0
  40. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  41. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/js/tina4helper.js +0 -0
  42. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/swagger/index.html +0 -0
  43. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  44. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/templates/errors/403.twig +0 -0
  45. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/templates/errors/404.twig +0 -0
  46. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/templates/errors/500.twig +0 -0
  47. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/templates/readme.md +0 -0
  48. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  49. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  50. {tina4_python-0.2.124 → tina4_python-0.2.125}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  51. {tina4_python-0.2.124 → tina4_python-0.2.125}/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.124
3
+ Version: 0.2.125
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "0.2.124"
3
+ version = "0.2.125"
4
4
  description = "Tina4Python - This is not another framework for Python"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
@@ -141,6 +141,9 @@ class CRUD:
141
141
  table_nice_name = Template.get_nice_label(table_name)
142
142
  twig_file = self.ensure_crud_template(table_name + ".twig")
143
143
 
144
+ if options is None:
145
+ options = {}
146
+
144
147
  if "primary_key" not in options:
145
148
  options["primary_key"] = "id"
146
149
 
@@ -165,9 +168,12 @@ class CRUD:
165
168
 
166
169
 
167
170
  # Use defined columns or fallback
168
- search_columns = self.columns
169
- if isinstance(search_columns, dict):
170
- search_columns = [c["name"] for c in search_columns if "name" in c]
171
+ if not "search_columns" in options:
172
+ search_columns = self.columns
173
+ if isinstance(search_columns, dict):
174
+ search_columns = [c["name"] for c in search_columns if "name" in c]
175
+ else:
176
+ search_columns = options["search_columns"]
171
177
 
172
178
  # Execute fetch with search
173
179
  result = self.dba.fetch(
@@ -4,6 +4,8 @@
4
4
  # License: MIT https://opensource.org/licenses/MIT
5
5
  #
6
6
  # flake8: noqa: E501
7
+ import ast
8
+ import html
7
9
  import os
8
10
  import re
9
11
  import json
@@ -33,6 +35,7 @@ class Template:
33
35
  Template.twig.add_extension('jinja2.ext.do')
34
36
  Template.twig.globals['RANDOM'] = RANDOM
35
37
  Template.twig.globals['json'] = json
38
+ Template.twig.filters['detect_image'] = Template.detect_image
36
39
  Template.twig.filters['json_encode'] = json.dumps
37
40
  Template.twig.filters['json_decode'] = json.loads
38
41
  Template.twig.filters['nice_label'] = Template.get_nice_label
@@ -79,7 +82,7 @@ class Template:
79
82
  """
80
83
  Recursively convert non-JSON-serializable objects:
81
84
  • datetime/date → ISO 8601 string
82
- • bytes base64 string
85
+ • bytes base64 string
83
86
  • dict/list/tuple/set → recursively processed
84
87
  Safe for deeply nested data (arrays of arrays, dicts in lists, etc.)
85
88
  """
@@ -145,81 +148,51 @@ class Template:
145
148
 
146
149
  @staticmethod
147
150
  def detect_image(value: Any) -> Dict[str, str]:
148
- """
149
- Detects if a string is an image (base64, data URL, or raw bytes)
150
- Returns: {"content": "<base64 without prefix>", "content_type": "image/jpeg|png|gif|webp"}
151
- """
152
- if value is None:
153
- return {"content": "", "content_type": ""}
154
-
155
- # Convert to string if it's bytes or something else
156
- if isinstance(value, (bytes, bytearray)):
157
- value = value.decode('latin1', errors='ignore')
158
- elif not isinstance(value, str):
159
- value = str(value)
151
+ if not value or len(value) <= 50:
152
+ return {"content": value, "content_type": ""}
160
153
 
161
- original = value.strip()
162
-
163
- # Case 1: JSON object like {"content": "...", "content_type": "..."}
164
- if original.startswith("{"):
165
- try:
166
- data = json.loads(original)
167
- if isinstance(data, dict) and "content" in data:
168
- content = data["content"]
169
- content_type = data.get("content_type", "")
170
- if content_type.startswith("image/"):
171
- return {
172
- "content": str(content).split(",", 1)[-1] if "," in content else str(content),
173
- "content_type": content_type
174
- }
175
- except:
176
- pass # not valid JSON, continue
177
-
178
- # Case 2: Data URL like data:image/png;base64,...
179
- if original.startswith("data:image/"):
154
+ if value[0] == '{' and value[-1] == '}':
180
155
  try:
181
- header, b64_data = original.split(",", 1)
182
- mime = header.split(";")[0].split(":", 1)[1] # extract image/png
183
- return {
184
- "content": b64_data,
185
- "content_type": mime
186
- }
187
- except:
188
- pass
189
-
190
- # Case 3: Pure base64 string with magic bytes detection
191
- b64 = original
192
-
193
- # Remove common prefixes if present
194
- if b64.startswith("data:"):
195
- b64 = b64.split(",", 1)[-1]
196
-
197
- # Clean whitespace/newlines
198
- b64 = b64.strip()
199
-
200
- # Try to detect by magic bytes (first few chars of base64)
201
- try:
202
- # Decode just the first 20 bytes to inspect magic
203
- sample = base64.b64decode(b64[:40] + "===", validate=True)
204
- except:
205
- return {"content": "", "content_type": ""}
206
-
207
- if sample.startswith(b'\xFF\xD8\xFF'):
208
- mime = "image/jpeg"
209
- elif sample.startswith(b'\x89PNG\r\n\x1A\n'):
210
- mime = "image/png"
211
- elif sample.startswith(b'GIF87a') or sample.startswith(b'GIF89a'):
212
- mime = "image/gif"
213
- elif sample.startswith(b'RIFF') and len(sample) >= 12 and sample[8:12] == b'WEBP':
214
- mime = "image/webp"
215
- elif sample.startswith(b'BM'): # BMP
216
- mime = "image/bmp"
217
- elif sample.startswith(b'\x00\x00\x01\x00'): # ICO
218
- mime = "image/x-icon"
156
+ value = html.unescape(value)
157
+ data = json.loads(value)
158
+ content = data.get('content', '')
159
+
160
+ if not content:
161
+ return {"content": value, "content_type": ""}
162
+
163
+ content_type = data.get('content_type', '')
164
+ if content_type.startswith('image/'):
165
+ mime_type = content_type.split('/')[1]
166
+ return {"content": content, "content_type": mime_type}
167
+
168
+ # Fallback to magic bytes if no content_type
169
+ if content[:4] == '/9j/':
170
+ mime_type = 'jpeg'
171
+ elif content[:11] == 'iVBORw0KGgo':
172
+ mime_type = 'png'
173
+ elif content[:6] == 'R0lGOD':
174
+ mime_type = 'gif'
175
+ elif content[:5] == 'UklGR':
176
+ mime_type = 'webp'
177
+ else:
178
+ return {"content": content, "content_type": ""}
179
+
180
+ return {"content": content, "content_type": mime_type}
181
+ except json.JSONDecodeError as e:
182
+ return {"content": str(e), "content_type": ""}
183
+
184
+
185
+ mime_type = "jpeg"
186
+ # Check magic bytes on value
187
+ if value[:4] == '/9j/':
188
+ mime_type = 'jpeg'
189
+ elif value[:11] == 'iVBORw0KGgo':
190
+ mime_type = 'png'
191
+ elif value[:6] == 'R0lGOD':
192
+ mime_type = 'gif'
193
+ elif value[:5] == 'UklGR':
194
+ mime_type = 'webp'
219
195
  else:
220
- return {"content": "", "content_type": ""}
196
+ return {"content": value, "content_type": mime_type}
221
197
 
222
- return {
223
- "content": b64,
224
- "content_type": mime
225
- }
198
+ return {"content": value, "content_type": mime_type}
@@ -48,26 +48,12 @@
48
48
 
49
49
  {# --- Detect image by magic bytes (no ternary, no starts with) --- #}
50
50
  {% set is_image = False %}
51
- {% set mime_type = 'jpeg' %}
51
+ {% set check_value = value | detect_image %}
52
52
 
53
- {% if value[0] == "{" %}
53
+ {% if check_value.content_type %}
54
54
  {% set is_image = True %}
55
- {% set value = value | json_decode %}
56
- {% set value = value.content %}
57
- {% else %}
58
- {% if value[:4] == '/9j/' %}
59
- {% set is_image = True %}
60
- {% set mime_type = 'jpeg' %}
61
- {% elif value[:11] == 'iVBORw0KGgo' %}
62
- {% set is_image = True %}
63
- {% set mime_type = 'png' %}
64
- {% elif value[:6] == 'R0lGOD' %}
65
- {% set is_image = True %}
66
- {% set mime_type = 'gif' %}
67
- {% elif value[:5] == 'UklGR' %}
68
- {% set is_image = True %}
69
- {% set mime_type = 'webp' %}
70
- {% endif %}
55
+ {% set value = check_value.content %}
56
+ {% set mime_type = check_value.mime_type %}
71
57
  {% endif %}
72
58
 
73
59
  {# --- Only show image if it's base64-like and long enough --- #}
@@ -132,34 +118,18 @@
132
118
  {% set name = column.name %}
133
119
  {% set value = record[name]|e %}
134
120
 
135
- {# --- Detect image by magic bytes (no ternary, no starts with) --- #}
136
121
  {% set is_image = False %}
137
- {% set mime_type = 'jpeg' %}
138
122
 
123
+ {% set check_value = value | detect_image %}
139
124
 
140
- {% if value | length > 50 and value != "" and value[0] == "{" %}
125
+ {% if check_value.content_type %}
141
126
  {% set is_image = True %}
142
- {% set value = record[name] | json_decode %}
143
- {% set value = value.content %}
144
- {% else %}
145
- {% if value|length > 50 and value[:4] == '/9j/' %}
146
- {% set is_image = True %}
147
- {% set mime_type = 'jpeg' %}
148
- {% elif value[:11] == 'iVBORw0KGgo' %}
149
- {% set is_image = True %}
150
- {% set mime_type = 'png' %}
151
- {% elif value[:6] == 'R0lGOD' %}
152
- {% set is_image = True %}
153
- {% set mime_type = 'gif' %}
154
- {% elif value[:5] == 'UklGR' %}
155
- {% set is_image = True %}
156
- {% set mime_type = 'webp' %}
157
- {% endif %}
127
+ {% set value = check_value.content %}
128
+ {% set mime_type = check_value.mime_type %}
158
129
  {% endif %}
159
-
160
130
  <td>
161
131
  {# --- Only show image if it's base64-like and long enough --- #}
162
- {% if is_image and value|length > 50 and value is string %}
132
+ {% if is_image %}
163
133
  {% set src = value %}
164
134
  {% if value[:10] != 'data:image/' %}
165
135
  {% set src = 'data:image/' ~ mime_type ~ ';base64,' ~ value %}
File without changes