tina4-python 3.10.11__tar.gz → 3.10.13__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 (139) hide show
  1. {tina4_python-3.10.11 → tina4_python-3.10.13}/PKG-INFO +1 -1
  2. {tina4_python-3.10.11 → tina4_python-3.10.13}/pyproject.toml +1 -1
  3. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/server.py +4 -0
  4. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/orm/model.py +9 -0
  5. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/websocket/backplane.py +96 -3
  6. {tina4_python-3.10.11 → tina4_python-3.10.13}/.gitignore +0 -0
  7. {tina4_python-3.10.11 → tina4_python-3.10.13}/README.md +0 -0
  8. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/CLAUDE.md +0 -0
  9. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/HtmlElement.py +0 -0
  10. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/Testing.py +0 -0
  11. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/__init__.py +0 -0
  12. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/ai/__init__.py +0 -0
  13. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/api/__init__.py +0 -0
  14. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/auth/__init__.py +0 -0
  15. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/cache/__init__.py +0 -0
  16. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/cli/__init__.py +0 -0
  17. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/container/__init__.py +0 -0
  18. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/request.py +0 -0
  24. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/response.py +0 -0
  25. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/core/router.py +0 -0
  26. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/dev_reload.py +0 -0
  40. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/dotenv/__init__.py +0 -0
  41. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/frond/FROND.md +0 -0
  42. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/frond/__init__.py +0 -0
  43. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/frond/engine.py +0 -0
  44. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/auth/meta.json +0 -0
  45. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  46. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/database/meta.json +0 -0
  47. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  48. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/error-overlay/meta.json +0 -0
  49. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  50. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/orm/meta.json +0 -0
  51. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  52. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  53. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/queue/meta.json +0 -0
  54. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  55. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/rest-api/meta.json +0 -0
  56. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  57. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/templates/meta.json +0 -0
  58. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  59. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  60. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/graphql/__init__.py +0 -0
  61. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/i18n/__init__.py +0 -0
  62. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/messenger/__init__.py +0 -0
  63. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/migration/__init__.py +0 -0
  64. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/migration/runner.py +0 -0
  65. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/orm/__init__.py +0 -0
  66. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/orm/fields.py +0 -0
  67. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/css/tina4.css +0 -0
  68. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/css/tina4.min.css +0 -0
  69. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/favicon.ico +0 -0
  70. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/images/logo.svg +0 -0
  71. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  72. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/js/frond.min.js +0 -0
  73. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  74. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/js/tina4.min.js +0 -0
  75. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/js/tina4js.min.js +0 -0
  76. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/swagger/index.html +0 -0
  77. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  78. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/query_builder/__init__.py +0 -0
  79. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/queue/__init__.py +0 -0
  80. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/queue_backends/__init__.py +0 -0
  81. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/queue_backends/kafka_backend.py +0 -0
  82. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/queue_backends/mongo_backend.py +0 -0
  83. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  84. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/__init__.py +0 -0
  85. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  86. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_badges.scss +0 -0
  87. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  88. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_cards.scss +0 -0
  89. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_forms.scss +0 -0
  90. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_grid.scss +0 -0
  91. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_modals.scss +0 -0
  92. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_nav.scss +0 -0
  93. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_reset.scss +0 -0
  94. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_tables.scss +0 -0
  95. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_typography.scss +0 -0
  96. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  97. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/_variables.scss +0 -0
  98. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/base.scss +0 -0
  99. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/colors.scss +0 -0
  100. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/scss/tina4css/tina4.scss +0 -0
  101. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/seeder/__init__.py +0 -0
  102. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/service/__init__.py +0 -0
  103. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/session/__init__.py +0 -0
  104. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/session_handlers/__init__.py +0 -0
  105. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  106. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/session_handlers/redis_handler.py +0 -0
  107. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/session_handlers/valkey_handler.py +0 -0
  108. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/swagger/__init__.py +0 -0
  109. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/components/crud.twig +0 -0
  110. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  111. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  112. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/docker/python/Dockerfile +0 -0
  113. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  114. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/302.twig +0 -0
  115. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/401.twig +0 -0
  116. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/403.twig +0 -0
  117. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/404.twig +0 -0
  118. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/500.twig +0 -0
  119. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/502.twig +0 -0
  120. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/503.twig +0 -0
  121. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/errors/base.twig +0 -0
  122. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/frontend/README.md +0 -0
  123. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/templates/readme.md +0 -0
  124. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/test_client/__init__.py +0 -0
  125. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  126. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  127. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  128. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  129. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  130. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  131. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  132. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  133. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  134. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  135. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  136. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  137. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/validator/__init__.py +0 -0
  138. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/websocket/__init__.py +0 -0
  139. {tina4_python-3.10.11 → tina4_python-3.10.13}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.10.11
3
+ Version: 3.10.13
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.10.11"
3
+ version = "3.10.13"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -966,6 +966,10 @@ async def app(scope: dict, receive, send):
966
966
  ttl = int(os.environ.get("TINA4_SESSION_TTL", "3600"))
967
967
  samesite = os.environ.get("TINA4_SESSION_SAMESITE", "Lax")
968
968
  response.header("set-cookie", f"tina4_session={sid}; Path=/; HttpOnly; SameSite={samesite}; Max-Age={ttl}")
969
+ # Probabilistic garbage collection (~1% of requests)
970
+ import random
971
+ if random.randint(1, 100) == 1:
972
+ request.session.gc()
969
973
  except Exception:
970
974
  pass
971
975
 
@@ -240,6 +240,9 @@ class ORM(metaclass=ORMMeta):
240
240
  if result.last_id and pk in self._fields:
241
241
  setattr(self, pk, result.last_id)
242
242
 
243
+ # ORM save is a discrete unit of work — always commit
244
+ db.commit()
245
+
243
246
  # Invalidate cached queries and relationship cache
244
247
  self.clear_cache()
245
248
  self._rel_cache = {}
@@ -264,6 +267,9 @@ class ORM(metaclass=ORMMeta):
264
267
  else:
265
268
  db.delete(table, f"{pk_db_col} = ?", [pk_value])
266
269
 
270
+ # ORM delete is a discrete unit of work — always commit
271
+ db.commit()
272
+
267
273
  def force_delete(self):
268
274
  """Hard delete, even if soft delete is enabled."""
269
275
  db = self._get_db()
@@ -276,6 +282,7 @@ class ORM(metaclass=ORMMeta):
276
282
  raise ValueError("Cannot delete: no primary key value")
277
283
 
278
284
  db.delete(table, f"{pk_db_col} = ?", [pk_value])
285
+ db.commit()
279
286
 
280
287
  def restore(self):
281
288
  """Restore a soft-deleted record."""
@@ -290,6 +297,7 @@ class ORM(metaclass=ORMMeta):
290
297
 
291
298
  db.update(table, {"deleted_at": None}, f"{pk_db_col} = ?", [pk_value])
292
299
  self.deleted_at = None
300
+ db.commit()
293
301
 
294
302
  # ── Finders ─────────────────────────────────────────────────
295
303
 
@@ -497,6 +505,7 @@ class ORM(metaclass=ORMMeta):
497
505
  sql = SQLTranslator.auto_increment_syntax(sql, engine)
498
506
 
499
507
  db.execute(sql)
508
+ db.commit()
500
509
  return True
501
510
 
502
511
  # ── Cached Queries ────────────────────────────────────────
@@ -100,6 +100,101 @@ class RedisBackplane(WebSocketBackplane):
100
100
  logger.info("RedisBackplane closed")
101
101
 
102
102
 
103
+ class NATSBackplane(WebSocketBackplane):
104
+ """NATS pub/sub backplane.
105
+
106
+ Requires the ``nats-py`` package (``pip install nats-py``). The import is
107
+ deferred so the rest of Tina4 works fine without it installed.
108
+
109
+ NATS is async-native, so we run an asyncio event loop in a background
110
+ thread for the subscription listener.
111
+ """
112
+
113
+ def __init__(self, url: str | None = None):
114
+ try:
115
+ import nats # noqa: F401
116
+ except ImportError:
117
+ raise ImportError(
118
+ "The 'nats-py' package is required for NATSBackplane. "
119
+ "Install it with: pip install nats-py"
120
+ )
121
+
122
+ self._url = url or os.environ.get(
123
+ "TINA4_WS_BACKPLANE_URL", "nats://localhost:4222"
124
+ )
125
+ self._nc = None
126
+ self._subs: dict[str, object] = {}
127
+ self._loop = None
128
+ self._thread = None
129
+ self._running = True
130
+ self._connect()
131
+ logger.info("NATSBackplane connected to %s", self._url)
132
+
133
+ def _connect(self):
134
+ """Connect to NATS in a background event loop."""
135
+ import asyncio
136
+ import nats
137
+
138
+ self._loop = asyncio.new_event_loop()
139
+
140
+ async def _do_connect():
141
+ self._nc = await nats.connect(self._url)
142
+
143
+ self._loop.run_until_complete(_do_connect())
144
+
145
+ # Run the event loop in a background thread for subscriptions
146
+ self._thread = threading.Thread(
147
+ target=self._loop.run_forever, daemon=True
148
+ )
149
+ self._thread.start()
150
+
151
+ def publish(self, channel: str, message: str) -> None:
152
+ import asyncio
153
+
154
+ async def _pub():
155
+ await self._nc.publish(channel, message.encode())
156
+
157
+ asyncio.run_coroutine_threadsafe(_pub(), self._loop).result(timeout=5)
158
+
159
+ def subscribe(self, channel: str, callback) -> None:
160
+ import asyncio
161
+
162
+ async def _sub():
163
+ async def handler(msg):
164
+ callback(msg.data.decode())
165
+
166
+ sub = await self._nc.subscribe(channel, cb=handler)
167
+ self._subs[channel] = sub
168
+
169
+ asyncio.run_coroutine_threadsafe(_sub(), self._loop).result(timeout=5)
170
+ logger.info("NATSBackplane subscribed to channel '%s'", channel)
171
+
172
+ def unsubscribe(self, channel: str) -> None:
173
+ import asyncio
174
+
175
+ sub = self._subs.pop(channel, None)
176
+ if sub:
177
+ asyncio.run_coroutine_threadsafe(
178
+ sub.unsubscribe(), self._loop
179
+ ).result(timeout=5)
180
+ logger.info("NATSBackplane unsubscribed from channel '%s'", channel)
181
+
182
+ def close(self) -> None:
183
+ import asyncio
184
+
185
+ self._running = False
186
+ if self._nc:
187
+ asyncio.run_coroutine_threadsafe(
188
+ self._nc.close(), self._loop
189
+ ).result(timeout=5)
190
+ if self._loop:
191
+ self._loop.call_soon_threadsafe(self._loop.stop)
192
+ if self._thread:
193
+ self._thread.join(timeout=2)
194
+ self._subs.clear()
195
+ logger.info("NATSBackplane closed")
196
+
197
+
103
198
  def create_backplane(url: str | None = None) -> WebSocketBackplane | None:
104
199
  """Factory that reads TINA4_WS_BACKPLANE and returns the appropriate
105
200
  backplane instance, or *None* if no backplane is configured.
@@ -112,9 +207,7 @@ def create_backplane(url: str | None = None) -> WebSocketBackplane | None:
112
207
  if backend == "redis":
113
208
  return RedisBackplane(url=url)
114
209
  elif backend == "nats":
115
- raise NotImplementedError(
116
- "NATS backplane is on the roadmap but not yet implemented."
117
- )
210
+ return NATSBackplane(url=url)
118
211
  elif backend == "":
119
212
  return None
120
213
  else:
File without changes