tina4-python 0.2.193__tar.gz → 0.2.195__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 (66) hide show
  1. {tina4_python-0.2.193 → tina4_python-0.2.195}/PKG-INFO +1 -1
  2. {tina4_python-0.2.193 → tina4_python-0.2.195}/pyproject.toml +2 -1
  3. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Router.py +12 -4
  4. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Session.py +94 -1
  5. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Webserver.py +6 -8
  6. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/__init__.py +8 -12
  7. {tina4_python-0.2.193 → tina4_python-0.2.195}/.gitignore +0 -0
  8. {tina4_python-0.2.193 → tina4_python-0.2.195}/README.md +0 -0
  9. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Api.py +0 -0
  10. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Auth.py +0 -0
  11. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/CLAUDE.md +0 -0
  12. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/CRUD.py +0 -0
  13. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Constant.py +0 -0
  14. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Database.py +0 -0
  15. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/DatabaseResult.py +0 -0
  16. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/DatabaseTypes.py +0 -0
  17. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Debug.py +0 -0
  18. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/DevReload.py +0 -0
  19. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Env.py +0 -0
  20. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/FieldTypes.py +0 -0
  21. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/HtmlElement.py +0 -0
  22. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Localization.py +0 -0
  23. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Messages.py +0 -0
  24. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/MiddleWare.py +0 -0
  25. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Migration.py +0 -0
  26. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/ORM.py +0 -0
  27. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Queue.py +0 -0
  28. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Request.py +0 -0
  29. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Response.py +0 -0
  30. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/ShellColors.py +0 -0
  31. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Swagger.py +0 -0
  32. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Template.py +0 -0
  33. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Testing.py +0 -0
  34. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/WSDL.py +0 -0
  35. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/Websocket.py +0 -0
  36. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/cli.py +0 -0
  37. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/messages.pot +0 -0
  38. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/css/readme.md +0 -0
  39. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/favicon.ico +0 -0
  40. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/images/403.png +0 -0
  41. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/images/404.png +0 -0
  42. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/images/500.png +0 -0
  43. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/images/logo.png +0 -0
  44. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/images/readme.md +0 -0
  45. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/js/readme.md +0 -0
  46. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/js/reconnecting-websocket.js +0 -0
  47. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/js/tina4helper.js +0 -0
  48. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/swagger/index.html +0 -0
  49. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  50. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/templates/components/crud.twig +0 -0
  51. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/templates/errors/403.twig +0 -0
  52. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/templates/errors/404.twig +0 -0
  53. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/templates/errors/500.twig +0 -0
  54. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/templates/readme.md +0 -0
  55. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  56. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  57. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  58. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  59. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  60. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  61. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  62. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  63. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  64. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  65. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  66. {tina4_python-0.2.193 → tina4_python-0.2.195}/tina4_python/translations/zh/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.193
3
+ Version: 0.2.195
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.193"
3
+ version = "0.2.195"
4
4
  description = "Tina4Python - This is not another framework for Python"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam",email = "andrevanzuydam@gmail.com"}
@@ -32,6 +32,7 @@ dev = [
32
32
  "pydoc-markdown>=4.8.2",
33
33
  "pytest>=8.3.5",
34
34
  "pytest-asyncio>=1.3.0",
35
+ "pytest-cov>=7.0.0",
35
36
  "python-keycloak>=5.8.1",
36
37
  "ruff>=0.11.9",
37
38
  "safety>=3.5.0",
@@ -354,6 +354,7 @@ class Router:
354
354
  tina4_python.tina4_current_request = {"url": url, "headers": headers}
355
355
 
356
356
  validated = False
357
+ has_form_token = False
357
358
  # we can add other methods later but right now we validate gets, posts and other risky methods
358
359
  if method in [Constant.TINA4_GET, Constant.TINA4_POST, Constant.TINA4_PUT, Constant.TINA4_PATCH,
359
360
  Constant.TINA4_DELETE]:
@@ -372,16 +373,19 @@ class Router:
372
373
  token = headers["authorization"].replace("Bearer", "").strip()
373
374
  if tina4_python.tina4_auth.valid(token):
374
375
  validated = True
376
+ has_form_token = True
375
377
 
376
378
  if request["params"] is not None and "formToken" in request["params"]:
377
379
  token = request["params"]["formToken"]
378
380
  if tina4_python.tina4_auth.valid(token):
379
381
  validated = True
382
+ has_form_token = True
380
383
 
381
384
  if request["body"] is not None and "formToken" in request["body"]:
382
385
  token = request["body"]["formToken"]
383
386
  if tina4_python.tina4_auth.valid(token):
384
387
  validated = True
388
+ has_form_token = True
385
389
 
386
390
  if request["body"] is not None and "formToken" in request["body"]:
387
391
  request["params"]["formToken"] = request["body"]["formToken"]
@@ -521,7 +525,8 @@ class Router:
521
525
  return Response(result.content, result.http_code, result.content_type)
522
526
 
523
527
  if result is not None:
524
- result.headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
528
+ if has_form_token:
529
+ result.headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
525
530
  if "cache" in route and route["cache"] is not None:
526
531
  if not route["cache"]["cached"]:
527
532
  result.headers["Cache-Control"] = "max-age=1, must-revalidate"
@@ -542,7 +547,9 @@ class Router:
542
547
  if result is None and route_matched:
543
548
  output = buffer.getvalue()
544
549
  if output:
545
- fresh_headers = {"FreshToken": tina4_python.tina4_auth.get_token({"path": url})}
550
+ fresh_headers = {}
551
+ if has_form_token:
552
+ fresh_headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
546
553
  try:
547
554
  return Response(json.loads(output), Constant.HTTP_OK, Constant.APPLICATION_JSON, fresh_headers)
548
555
  except Exception:
@@ -566,10 +573,11 @@ class Router:
566
573
  )
567
574
 
568
575
  twig_headers = {
569
- "FreshToken": tina4_python.tina4_auth.get_token({"path": url}),
570
576
  "Cache-Control": "max-age=-1, public",
571
577
  "Pragma": "no-cache"
572
578
  }
579
+ if has_form_token:
580
+ twig_headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
573
581
  content = Template.render_twig_template(twig_file, {"request": tina4_python.tina4_current_request})
574
582
  if content != "":
575
583
  return Response(content, Constant.HTTP_OK, Constant.TEXT_HTML, twig_headers)
@@ -579,7 +587,7 @@ class Router:
579
587
  "errors/404.twig", {"server": {"url": url}})
580
588
  return Response(content, Constant.HTTP_NOT_FOUND, Constant.TEXT_HTML)
581
589
 
582
- result.headers["FreshToken"] = tina4_python.tina4_auth.get_token({"path": url})
590
+ # FreshToken already set on line 524 inside the route loop
583
591
  return result
584
592
 
585
593
  @staticmethod
@@ -30,7 +30,7 @@ Typical usage inside a route handler::
30
30
  """
31
31
 
32
32
  __all__ = [
33
- "Session", "SessionHandler", "SessionFileHandler",
33
+ "Session", "LazySession", "SessionHandler", "SessionFileHandler",
34
34
  "SessionRedisHandler", "SessionValkeyHandler",
35
35
  "SessionMongoHandler",
36
36
  ]
@@ -685,3 +685,96 @@ class Session:
685
685
  for key, value in self.session_values.items():
686
686
  if key != "expires":
687
687
  yield key, value
688
+
689
+
690
+ class LazySession:
691
+ """Proxy that defers expensive Session creation until first use.
692
+
693
+ Creating and starting a ``Session`` requires RSA key signing (~1ms
694
+ per call) which dominates request latency for API routes that never
695
+ touch the session. ``LazySession`` wraps the session creation
696
+ parameters and only instantiates the real ``Session`` when a method
697
+ like ``set()``, ``get()``, or ``load()`` is called.
698
+
699
+ The ``activated`` property lets the response builder know whether
700
+ to emit a ``Set-Cookie`` header.
701
+ """
702
+
703
+ def __init__(self, name, path, handler, cookies):
704
+ self._name = name
705
+ self._path = path
706
+ self._handler = handler
707
+ self._cookies = cookies
708
+ self._real = None
709
+
710
+ @property
711
+ def activated(self):
712
+ """True if the real Session has been created."""
713
+ return self._real is not None
714
+
715
+ def _activate(self):
716
+ """Create and start/load the real Session on first access."""
717
+ if self._real is None:
718
+ self._real = Session(self._name, self._path, self._handler)
719
+ if self._name in self._cookies:
720
+ self._real.load(self._cookies[self._name])
721
+ else:
722
+ self._cookies[self._name] = self._real.start()
723
+ return self._real
724
+
725
+ # --- Forwarded Session API ---
726
+
727
+ @property
728
+ def session_name(self):
729
+ return self._name
730
+
731
+ @property
732
+ def session_values(self):
733
+ if self._real is None:
734
+ return {}
735
+ return self._real.session_values
736
+
737
+ @property
738
+ def session_hash(self):
739
+ if self._real is None:
740
+ return ""
741
+ return self._real.session_hash
742
+
743
+ @session_hash.setter
744
+ def session_hash(self, value):
745
+ self._activate().session_hash = value
746
+
747
+ def start(self, session_hash=None):
748
+ result = self._activate().start(session_hash)
749
+ # Keep cookie dict in sync with the (possibly new) session hash
750
+ self._cookies[self._name] = result
751
+ return result
752
+
753
+ def load(self, session_hash):
754
+ self._activate().load(session_hash)
755
+ # Keep cookie dict in sync with loaded session hash
756
+ self._cookies[self._name] = self._real.session_hash
757
+
758
+ def set(self, key, value):
759
+ return self._activate().set(key, value)
760
+
761
+ def get(self, key):
762
+ return self._activate().get(key)
763
+
764
+ def unset(self, key):
765
+ return self._activate().unset(key)
766
+
767
+ def close(self):
768
+ if self._real is not None:
769
+ return self._real.close()
770
+ return True
771
+
772
+ def save(self):
773
+ if self._real is not None:
774
+ return self._real.save()
775
+ return True
776
+
777
+ def __iter__(self):
778
+ if self._real is not None:
779
+ return iter(self._real)
780
+ return iter([])
@@ -434,9 +434,10 @@ class Webserver:
434
434
  self.send_header("Content-Type", response.content_type or "text/html", headers)
435
435
  await self.send_basic_headers(headers)
436
436
 
437
- # Preserve session cookie
437
+ # Preserve session cookie (only if session was used)
438
438
  session_name = os.getenv("TINA4_SESSION", "PY_SESS")
439
- if session_name in self.cookies:
439
+ session_activated = not hasattr(self.session, 'activated') or self.session.activated
440
+ if session_activated and session_name in self.cookies:
440
441
  self.send_header("Set-Cookie", f"{session_name}={self.cookies[session_name]}", headers)
441
442
 
442
443
  # Custom headers from route
@@ -585,14 +586,11 @@ class Webserver:
585
586
  name, val = part.strip().split("=", 1)
586
587
  self.cookies[name] = val
587
588
 
589
+ from tina4_python.Session import LazySession
588
590
  session_name = os.getenv("TINA4_SESSION", "PY_SESS")
589
591
  session_folder = os.getenv("TINA4_SESSION_FOLDER", os.path.join(tina4_python.root_path, "sessions"))
590
- self.session = Session(session_name, session_folder)
591
-
592
- if session_name in self.cookies:
593
- self.session.load(self.cookies[session_name])
594
- else:
595
- self.cookies[session_name] = self.session.start()
592
+ session_handler = os.getenv("TINA4_SESSION_HANDLER", "SessionFileHandler")
593
+ self.session = LazySession(session_name, session_folder, session_handler, self.cookies)
596
594
 
597
595
  # ------------------------------------------------------------------
598
596
  # Route or WebSocket
@@ -21,8 +21,6 @@ Just `pip install tina4-python` and run your project – everything just works.
21
21
  """
22
22
  import asyncio
23
23
  import os
24
- if os.getenv("TINA4_DEBUG_LEVEL", "") == "":
25
- os.environ["TINA4_DEBUG_LEVEL"] = "DEBUG"
26
24
 
27
25
  import shutil
28
26
  import importlib
@@ -46,7 +44,7 @@ from tina4_python.Auth import Auth
46
44
  from tina4_python.Debug import Debug
47
45
  from tina4_python.Debug import setup_logging
48
46
  from tina4_python.ShellColors import ShellColors
49
- from tina4_python.Session import Session
47
+ from tina4_python.Session import Session, LazySession
50
48
  from tina4_python.HtmlElement import add_html_helpers
51
49
  from tina4_python import ShellColors
52
50
  from tina4_python.Constant import TINA4_LOG_INFO, TINA4_LOG_ALL, TINA4_LOG_DEBUG
@@ -428,15 +426,13 @@ async def app(scope, receive, send):
428
426
 
429
427
  webserver.cookies = cookie_list
430
428
 
431
- webserver.session = Session(os.getenv("TINA4_SESSION", "PY_SESS"),
432
- os.getenv("TINA4_SESSION_FOLDER", root_path + os.sep + "sessions"),
433
- os.getenv("TINA4_SESSION_HANDLER", "SessionFileHandler")
434
- )
435
-
436
- if os.getenv("TINA4_SESSION", "PY_SESS") in webserver.cookies:
437
- webserver.session.load(webserver.cookies[os.getenv("TINA4_SESSION", "PY_SESS")])
438
- else:
439
- webserver.cookies[os.getenv("TINA4_SESSION", "PY_SESS")] = webserver.session.start()
429
+ session_name = os.getenv("TINA4_SESSION", "PY_SESS")
430
+ webserver.session = LazySession(
431
+ session_name,
432
+ os.getenv("TINA4_SESSION_FOLDER", root_path + os.sep + "sessions"),
433
+ os.getenv("TINA4_SESSION_HANDLER", "SessionFileHandler"),
434
+ webserver.cookies,
435
+ )
440
436
 
441
437
  tina4_response, tina4_headers = await webserver.get_response(webserver.method, scope=scope, reader=receive, writer=send, asgi_response=True)
442
438
 
File without changes