tina4-python 0.2.159__tar.gz → 0.2.161__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.159 → tina4_python-0.2.161}/PKG-INFO +5 -1
  2. {tina4_python-0.2.159 → tina4_python-0.2.161}/README.md +4 -0
  3. {tina4_python-0.2.159 → tina4_python-0.2.161}/pyproject.toml +1 -1
  4. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Queue.py +71 -39
  5. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Webserver.py +2 -2
  6. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Websocket.py +2 -2
  7. {tina4_python-0.2.159 → tina4_python-0.2.161}/.gitignore +0 -0
  8. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Api.py +0 -0
  9. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Auth.py +0 -0
  10. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/CRUD.py +0 -0
  11. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Constant.py +0 -0
  12. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Database.py +0 -0
  13. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/DatabaseResult.py +0 -0
  14. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/DatabaseTypes.py +0 -0
  15. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Debug.py +0 -0
  16. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Env.py +0 -0
  17. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/FieldTypes.py +0 -0
  18. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/HtmlElement.py +0 -0
  19. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Localization.py +0 -0
  20. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Messages.py +0 -0
  21. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/MiddleWare.py +0 -0
  22. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Migration.py +0 -0
  23. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/ORM.py +0 -0
  24. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Request.py +0 -0
  25. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Response.py +0 -0
  26. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Router.py +0 -0
  27. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Session.py +0 -0
  28. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/ShellColors.py +0 -0
  29. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Swagger.py +0 -0
  30. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Template.py +0 -0
  31. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/Testing.py +0 -0
  32. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/WSDL.py +0 -0
  33. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/__init__.py +0 -0
  34. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/cli.py +0 -0
  35. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/messages.pot +0 -0
  36. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/css/readme.md +0 -0
  37. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/favicon.ico +0 -0
  38. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/images/403.png +0 -0
  39. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/images/404.png +0 -0
  40. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/images/500.png +0 -0
  41. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/images/logo.png +0 -0
  42. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/images/readme.md +0 -0
  43. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/js/readme.md +0 -0
  44. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  45. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/js/tina4helper.js +0 -0
  46. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/swagger/index.html +0 -0
  47. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  48. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/templates/components/crud.twig +0 -0
  49. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/templates/errors/403.twig +0 -0
  50. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/templates/errors/404.twig +0 -0
  51. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/templates/errors/500.twig +0 -0
  52. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/templates/readme.md +0 -0
  53. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  54. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  55. {tina4_python-0.2.159 → tina4_python-0.2.161}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  56. {tina4_python-0.2.159 → tina4_python-0.2.161}/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.159
3
+ Version: 0.2.161
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
@@ -107,6 +107,10 @@ https://tina4.com/
107
107
  MIT © 2007 – 2025 Tina4 Stack
108
108
  https://opensource.org/licenses/MIT
109
109
 
110
+ ## Testing
111
+
112
+ uv run pytest --verbose
113
+
110
114
  ---
111
115
 
112
116
  **Tina4** – The framework that keeps out of the way of your coding.
@@ -87,6 +87,10 @@ https://tina4.com/
87
87
  MIT © 2007 – 2025 Tina4 Stack
88
88
  https://opensource.org/licenses/MIT
89
89
 
90
+ ## Testing
91
+
92
+ uv run pytest --verbose
93
+
90
94
  ---
91
95
 
92
96
  **Tina4** – The framework that keeps out of the way of your coding.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "0.2.159"
3
+ version = "0.2.161"
4
4
  description = "Tina4Python - This is not another framework for Python"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
@@ -55,7 +55,7 @@ class Message:
55
55
  delivery_tag: str
56
56
 
57
57
  class Queue:
58
- def __init__(self, config=None, topic="default-queue", callback=None):
58
+ def __init__(self, config=None, topic="default-queue", callback=None, batch_size=1):
59
59
  if config is None:
60
60
  config = Config()
61
61
  self.config = config
@@ -63,6 +63,7 @@ class Queue:
63
63
  self.callback = callback
64
64
  self.producer = None
65
65
  self.consumer = None
66
+ self.batch_size = batch_size
66
67
  init_method = f"init_{config.queue_type.replace('-', '_')}"
67
68
  getattr(self, init_method)()
68
69
 
@@ -101,53 +102,81 @@ class Queue:
101
102
  delivery_callback(self.producer, e, None)
102
103
  return e
103
104
 
104
- def consume(self, acknowledge: bool = True) -> Generator[Message, None, None]:
105
+ def consume(self, acknowledge: bool = True) -> Generator[Message | List[Message], None, None]:
105
106
  """
106
- Generator that yields messages one by one.
107
+ Generator that continuously yields messages from the queue as they arrive.
107
108
  Use like:
108
109
  for msg in queue.consume():
109
110
  print(msg.data)
111
+ If a callback was provided in __init__, it will also be called for each message.
110
112
  """
111
113
  prefix = self.get_prefix()
114
+ is_batch = self.batch_size > 1
115
+ count_messages = 0
112
116
  try:
113
- if self.config.queue_type == "litequeue":
114
- msg = self.consumer.pop()
115
- if msg:
116
- data = json.loads(msg.data)
117
- response = Message(msg.message_id, data["msg"], data["user_id"], msg.status, msg.in_time, "0")
118
- if acknowledge:
119
- self.consumer.done(msg.message_id)
120
- msg = self.consumer.get(msg.message_id)
121
- response.status = int(msg.status)
122
- yield response
117
+ message_found = True
118
+ batch = []
119
+ while message_found and count_messages < self.batch_size:
120
+ response = None
121
+ message_found = False
123
122
 
124
- elif self.config.queue_type == "mongo-queue-service":
125
- msg = self.consumer.next(channel=prefix + self.topic)
126
- if msg:
127
- data = msg.payload
128
- response = Message(data["message_id"], data["msg"], data["user_id"], 2 if acknowledge else 1, msg.queued_at, "0")
129
- if acknowledge:
130
- msg.complete()
131
- yield response
123
+ if self.config.queue_type == "litequeue":
124
+ msg = self.consumer.pop()
125
+ if msg:
126
+ message_found = True
127
+ data = json.loads(msg.data)
128
+ response = Message(msg.message_id, data["msg"], data["user_id"], msg.status, msg.in_time, "0")
129
+ if acknowledge:
130
+ self.consumer.done(msg.message_id)
131
+ updated = self.consumer.get(msg.message_id)
132
+ if updated:
133
+ response.status = int(updated.status)
132
134
 
133
- elif self.config.queue_type == "rabbitmq":
134
- method, _, body = self.consumer.basic_get(queue=prefix + self.topic, auto_ack=acknowledge)
135
- if method:
136
- data = json.loads(body)
137
- response = Message(data["message_id"], data["msg"], data["user_id"], 2 if acknowledge else 1, data["in_time"], method.delivery_tag)
138
- yield response
135
+ elif self.config.queue_type == "mongo-queue-service":
136
+ msg = self.consumer.next(channel=prefix + self.topic)
137
+ if msg:
138
+ message_found = True
139
+ data = msg.payload
140
+ response = Message(data["message_id"], data["msg"], data["user_id"], 2 if acknowledge else 1, msg.queued_at, "0")
141
+ if acknowledge:
142
+ msg.complete()
139
143
 
140
- elif self.config.queue_type == "kafka":
141
- msg = self.consumer.poll(0.1) # non-blocking poll
142
- if msg and not msg.error():
143
- data = json.loads(msg.value().decode('utf-8'))
144
- response = Message(data["message_id"], data["msg"], data["user_id"], 2 if acknowledge else 1, data["in_time"], str(msg.offset()))
145
- if acknowledge:
146
- self.consumer.commit()
147
- yield response
144
+ elif self.config.queue_type == "rabbitmq":
145
+ method, _, body = self.consumer.basic_get(queue=prefix + self.topic, auto_ack=acknowledge)
146
+ if method:
147
+ message_found = True
148
+ data = json.loads(body)
149
+ response = Message(data["message_id"], data["msg"], data["user_id"], 2 if acknowledge else 1, data["in_time"], method.delivery_tag)
150
+
151
+ elif self.config.queue_type == "kafka":
152
+ msg = self.consumer.poll(0.1)
153
+ if msg and not msg.error():
154
+ message_found = True
155
+ data = json.loads(msg.value().decode('utf-8'))
156
+ response = Message(data["message_id"], data["msg"], data["user_id"], 2 if acknowledge else 1, data["in_time"], str(msg.offset()))
157
+ if acknowledge:
158
+ self.consumer.commit()
159
+
160
+ if message_found:
161
+ count_messages += 1
162
+ batch.append(response)
163
+ if self.callback:
164
+ try:
165
+ self.callback(response)
166
+ except Exception as e:
167
+ Debug.error("Failed to run queue callback", str(e))
168
+ else:
169
+ # No message available right now — brief sleep to avoid busy loop
170
+ time.sleep(0.05)
148
171
 
172
+ if len(batch) > 0:
173
+ if not is_batch:
174
+ yield batch[0]
175
+ else:
176
+ yield batch
149
177
  except Exception as e:
150
178
  Debug.error(f"Error consuming {self.topic}: {e}")
179
+ raise # Re-raise to stop consumption on fatal error
151
180
 
152
181
  # init methods remain unchanged
153
182
  def init_litequeue(self):
@@ -238,18 +267,21 @@ class Consumer:
238
267
  self.poll_interval = poll_interval
239
268
 
240
269
  def messages(self) -> Generator[Message, None, None]:
241
- """Generator that yields messages from all configured queues forever"""
242
270
  Debug.debug("Consuming from queues", [q.topic for q in self.queues])
243
271
  while True:
272
+ emptied_count = 0
244
273
  for queue in self.queues:
274
+ drained = False
245
275
  for message in queue.consume(self.acknowledge):
246
276
  yield message
247
- time.sleep(self.poll_interval)
277
+ drained = True
278
+ if not drained:
279
+ emptied_count += 1
280
+ if emptied_count == len(self.queues):
281
+ time.sleep(self.poll_interval)
248
282
 
249
283
  def run_forever(self):
250
284
  """Simple blocking runner — easy to use"""
251
285
  for message in self.messages():
252
286
  Debug.info(f"Received message: {message.message_id} -> {message.data}")
253
- # Do something with message here
254
- # Or just pass to a callback if needed
255
287
 
@@ -534,9 +534,9 @@ class Webserver:
534
534
  # Route or WebSocket
535
535
  # ------------------------------------------------------------------
536
536
  if "sec-websocket-key" in lowercase_headers:
537
- await self.get_response(self.method, reader, writer)
537
+ await self.get_response(self.method, scope={'type': 'websocket'}, reader=reader, writer=writer)
538
538
  else:
539
- response_bytes = await self.get_response(self.method, reader, writer)
539
+ response_bytes = await self.get_response(self.method, scope={'type': 'http'}, reader=reader, writer=writer)
540
540
  if response_bytes:
541
541
  writer.write(response_bytes)
542
542
  await writer.drain()
@@ -32,12 +32,12 @@ class Websocket:
32
32
  else:
33
33
  if os.name == "nt":
34
34
  connection = await self.server.AioServer.accept(
35
- sock=TransportSocket(self.request.transport.transport._sock), # not working properly
35
+ sock=TransportSocket(self.request.asgi_writer.transport._sock), # not working properly
36
36
  headers=self.request.headers
37
37
  )
38
38
  else:
39
39
  connection = await self.server.AioServer.accept(
40
- sock=self.request.transport.get_extra_info('socket').dup(),
40
+ sock=self.request.asgi_writer.get_extra_info('socket').dup(),
41
41
  headers=self.request.headers
42
42
  )
43
43
  return connection