tina4-python 3.10.34__tar.gz → 3.10.39__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 (144) hide show
  1. {tina4_python-3.10.34 → tina4_python-3.10.39}/.gitignore +0 -3
  2. {tina4_python-3.10.34 → tina4_python-3.10.39}/PKG-INFO +1 -1
  3. {tina4_python-3.10.34 → tina4_python-3.10.39}/pyproject.toml +1 -1
  4. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/__init__.py +1 -1
  5. tina4_python-3.10.39/tina4_python/ai/__init__.py +312 -0
  6. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/auth/__init__.py +47 -6
  7. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/cli/__init__.py +10 -33
  8. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/router.py +7 -2
  9. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/server.py +3 -3
  10. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/dev_admin/__init__.py +322 -8
  11. tina4_python-3.10.39/tina4_python/dev_admin/metrics.py +513 -0
  12. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/orm/model.py +12 -2
  13. tina4_python-3.10.34/tina4_python/ai/__init__.py +0 -412
  14. {tina4_python-3.10.34 → tina4_python-3.10.39}/README.md +0 -0
  15. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/CLAUDE.md +0 -0
  16. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/HtmlElement.py +0 -0
  17. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/Testing.py +0 -0
  18. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/api/__init__.py +0 -0
  19. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/cache/__init__.py +0 -0
  20. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/container/__init__.py +0 -0
  21. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/__init__.py +0 -0
  22. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/cache.py +0 -0
  23. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/constants.py +0 -0
  24. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/events.py +0 -0
  25. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/middleware.py +0 -0
  26. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/request.py +0 -0
  27. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/core/response.py +0 -0
  28. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/adapter.py +0 -0
  31. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/mssql.py +0 -0
  34. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/mysql.py +0 -0
  35. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/postgres.py +0 -0
  37. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/database/sqlite.py +0 -0
  38. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/debug/__init__.py +0 -0
  39. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/debug/error_overlay.py +0 -0
  40. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/dev_reload.py +0 -0
  41. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/dotenv/__init__.py +0 -0
  42. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/frond/FROND.md +0 -0
  43. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/frond/__init__.py +0 -0
  44. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/frond/engine.py +0 -0
  45. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/auth/meta.json +0 -0
  46. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  47. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/database/meta.json +0 -0
  48. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  49. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/error-overlay/meta.json +0 -0
  50. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  51. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/orm/meta.json +0 -0
  52. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  53. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  54. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/queue/meta.json +0 -0
  55. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  56. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/rest-api/meta.json +0 -0
  57. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  58. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/templates/meta.json +0 -0
  59. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  60. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  61. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/graphql/__init__.py +0 -0
  62. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/i18n/__init__.py +0 -0
  63. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/mcp/__init__.py +0 -0
  64. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/mcp/protocol.py +0 -0
  65. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/mcp/tools.py +0 -0
  66. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/messenger/__init__.py +0 -0
  67. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/migration/__init__.py +0 -0
  68. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/migration/runner.py +0 -0
  69. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/orm/__init__.py +0 -0
  70. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/orm/fields.py +0 -0
  71. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/css/tina4.css +0 -0
  72. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/css/tina4.min.css +0 -0
  73. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/favicon.ico +0 -0
  74. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/images/logo.svg +0 -0
  75. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  76. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/frond.min.js +0 -0
  77. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  78. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/tina4.min.js +0 -0
  79. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/js/tina4js.min.js +0 -0
  80. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/swagger/index.html +0 -0
  81. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  82. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/query_builder/__init__.py +0 -0
  83. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue/__init__.py +0 -0
  84. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/__init__.py +0 -0
  85. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/kafka_backend.py +0 -0
  86. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/mongo_backend.py +0 -0
  87. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  88. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/__init__.py +0 -0
  89. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  90. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_badges.scss +0 -0
  91. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  92. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_cards.scss +0 -0
  93. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_forms.scss +0 -0
  94. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_grid.scss +0 -0
  95. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_modals.scss +0 -0
  96. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_nav.scss +0 -0
  97. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_reset.scss +0 -0
  98. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_tables.scss +0 -0
  99. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_typography.scss +0 -0
  100. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  101. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/_variables.scss +0 -0
  102. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/base.scss +0 -0
  103. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/colors.scss +0 -0
  104. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/scss/tina4css/tina4.scss +0 -0
  105. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/seeder/__init__.py +0 -0
  106. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/service/__init__.py +0 -0
  107. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session/__init__.py +0 -0
  108. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/__init__.py +0 -0
  109. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  110. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/redis_handler.py +0 -0
  111. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/session_handlers/valkey_handler.py +0 -0
  112. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/swagger/__init__.py +0 -0
  113. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/components/crud.twig +0 -0
  114. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  115. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  116. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/python/Dockerfile +0 -0
  117. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  118. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/302.twig +0 -0
  119. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/401.twig +0 -0
  120. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/403.twig +0 -0
  121. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/404.twig +0 -0
  122. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/500.twig +0 -0
  123. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/502.twig +0 -0
  124. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/503.twig +0 -0
  125. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/errors/base.twig +0 -0
  126. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/frontend/README.md +0 -0
  127. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/templates/readme.md +0 -0
  128. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/test_client/__init__.py +0 -0
  129. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  130. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  131. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  132. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  133. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  134. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  135. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  136. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  137. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  138. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  139. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  140. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  141. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/validator/__init__.py +0 -0
  142. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/websocket/__init__.py +0 -0
  143. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/websocket/backplane.py +0 -0
  144. {tina4_python-3.10.34 → tina4_python-3.10.39}/tina4_python/wsdl/__init__.py +0 -0
@@ -60,9 +60,6 @@ __pycache__/
60
60
  *.so
61
61
  build/
62
62
  .pytest_cache/
63
- demo/.env
64
63
  example/.env
65
- demo/data/*.db
66
- demo/logs/*.log
67
64
  .claude/settings.local.json
68
65
  .claude/worktrees/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.10.34
3
+ Version: 3.10.39
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.34"
3
+ version = "3.10.39"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.10.24"
11
+ __version__ = "3.10.38"
12
12
 
13
13
  # ── HTTP Constants ──
14
14
  from tina4_python.core.constants import ( # noqa: E402, F401
@@ -0,0 +1,312 @@
1
+ # Tina4 AI — Install AI coding assistant context files.
2
+ """
3
+ Simple menu-driven installer for AI tool context files.
4
+ The user picks which tools they use, we install the appropriate files.
5
+
6
+ from tina4_python.ai import show_menu, install_selected
7
+ """
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ from pathlib import Path
12
+
13
+
14
+ # Ordered list of supported AI tools
15
+ AI_TOOLS = [
16
+ {"name": "claude-code", "description": "Claude Code", "context_file": "CLAUDE.md", "config_dir": ".claude"},
17
+ {"name": "cursor", "description": "Cursor", "context_file": ".cursorules", "config_dir": ".cursor"},
18
+ {"name": "copilot", "description": "GitHub Copilot", "context_file": ".github/copilot-instructions.md", "config_dir": ".github"},
19
+ {"name": "windsurf", "description": "Windsurf", "context_file": ".windsurfrules", "config_dir": None},
20
+ {"name": "aider", "description": "Aider", "context_file": "CONVENTIONS.md", "config_dir": None},
21
+ {"name": "cline", "description": "Cline", "context_file": ".clinerules", "config_dir": None},
22
+ {"name": "codex", "description": "OpenAI Codex", "context_file": "AGENTS.md", "config_dir": None},
23
+ ]
24
+
25
+
26
+ def is_installed(root: str, tool: dict) -> bool:
27
+ """Check if a tool's context file already exists."""
28
+ return (Path(root).resolve() / tool["context_file"]).exists()
29
+
30
+
31
+ def show_menu(root: str = ".") -> str:
32
+ """Print the numbered menu and return user input."""
33
+ root = str(Path(root).resolve())
34
+ green = "\033[32m"
35
+ reset = "\033[0m"
36
+
37
+ print("\n Tina4 AI Context Installer\n")
38
+ for i, tool in enumerate(AI_TOOLS, 1):
39
+ installed = is_installed(root, tool)
40
+ marker = f" {green}[installed]{reset}" if installed else ""
41
+ print(f" {i}. {tool['description']:<20s} {tool['context_file']}{marker}")
42
+
43
+ # tina4-ai tools option
44
+ tina4_ai_installed = shutil.which("mdview") is not None
45
+ marker = f" {green}[installed]{reset}" if tina4_ai_installed else ""
46
+ print(f" 8. Install tina4-ai tools (requires Python){marker}")
47
+ print()
48
+ return input(" Select (comma-separated, or 'all'): ").strip()
49
+
50
+
51
+ def install_selected(root: str, selection: str) -> list[str]:
52
+ """Install context files for the selected tools.
53
+
54
+ selection: comma-separated numbers like "1,2,3" or "all"
55
+ Returns list of created/updated file paths.
56
+ """
57
+ root_path = Path(root).resolve()
58
+ created = []
59
+
60
+ if selection.lower() == "all":
61
+ indices = list(range(len(AI_TOOLS)))
62
+ install_tina4_ai = True
63
+ else:
64
+ parts = [s.strip() for s in selection.split(",") if s.strip()]
65
+ indices = []
66
+ install_tina4_ai = False
67
+ for p in parts:
68
+ try:
69
+ n = int(p)
70
+ if n == 8:
71
+ install_tina4_ai = True
72
+ elif 1 <= n <= len(AI_TOOLS):
73
+ indices.append(n - 1)
74
+ except ValueError:
75
+ pass
76
+
77
+ context = generate_context()
78
+
79
+ for idx in indices:
80
+ tool = AI_TOOLS[idx]
81
+ files = _install_for_tool(root_path, tool, context)
82
+ created.extend(files)
83
+
84
+ if install_tina4_ai:
85
+ _install_tina4_ai()
86
+
87
+ return created
88
+
89
+
90
+ def install_all(root: str = ".") -> list[str]:
91
+ """Install context for all AI tools (non-interactive)."""
92
+ return install_selected(root, "all")
93
+
94
+
95
+ def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
96
+ """Install context file for a single tool."""
97
+ created = []
98
+ context_path = root / tool["context_file"]
99
+
100
+ # Create directories
101
+ if tool.get("config_dir"):
102
+ (root / tool["config_dir"]).mkdir(parents=True, exist_ok=True)
103
+ context_path.parent.mkdir(parents=True, exist_ok=True)
104
+
105
+ # Always overwrite — user chose to install
106
+ context_path.write_text(context, encoding="utf-8")
107
+ action = "Updated" if context_path.exists() else "Installed"
108
+ rel = str(context_path.relative_to(root))
109
+ created.append(rel)
110
+ print(f" \033[32m✓\033[0m {action} {rel}")
111
+
112
+ # Claude-specific extras
113
+ if tool["name"] == "claude-code":
114
+ skills = _install_claude_skills(root)
115
+ created.extend(skills)
116
+
117
+ return created
118
+
119
+
120
+ def _install_tina4_ai():
121
+ """Install tina4-ai package (provides mdview for markdown viewing)."""
122
+ print(" Installing tina4-ai tools...")
123
+ for cmd in ["pip3", "pip"]:
124
+ if shutil.which(cmd):
125
+ try:
126
+ result = subprocess.run(
127
+ [cmd, "install", "--upgrade", "tina4-ai"],
128
+ capture_output=True, text=True, timeout=60,
129
+ )
130
+ if result.returncode == 0:
131
+ print(" \033[32m✓\033[0m Installed tina4-ai (mdview)")
132
+ return
133
+ else:
134
+ print(f" \033[33m!\033[0m {cmd} failed: {result.stderr.strip()[:100]}")
135
+ except (subprocess.TimeoutExpired, OSError):
136
+ continue
137
+ print(" \033[33m!\033[0m Python/pip not available — skip tina4-ai")
138
+
139
+
140
+ def _install_claude_skills(root: Path) -> list[str]:
141
+ """Copy Claude Code skill files from the framework's templates."""
142
+ created = []
143
+ commands_dir = root / ".claude" / "commands"
144
+ commands_dir.mkdir(parents=True, exist_ok=True)
145
+
146
+ pkg_dir = Path(__file__).parent.parent
147
+ source_dirs = [pkg_dir / "templates" / "ai" / "claude-commands"]
148
+
149
+ for source_dir in source_dirs:
150
+ if source_dir.is_dir():
151
+ for skill_file in source_dir.glob("*.md"):
152
+ target = commands_dir / skill_file.name
153
+ target.write_text(skill_file.read_text(encoding="utf-8"), encoding="utf-8")
154
+ rel = str(target.relative_to(root))
155
+ created.append(rel)
156
+
157
+ # Copy skill directories from framework .claude/skills/
158
+ framework_root = pkg_dir.parent
159
+ framework_skills_dir = framework_root / ".claude" / "skills"
160
+ if framework_skills_dir.is_dir():
161
+ target_skills_dir = root / ".claude" / "skills"
162
+ target_skills_dir.mkdir(parents=True, exist_ok=True)
163
+ for skill_dir in framework_skills_dir.iterdir():
164
+ if skill_dir.is_dir():
165
+ target_dir = target_skills_dir / skill_dir.name
166
+ if target_dir.exists():
167
+ shutil.rmtree(target_dir)
168
+ shutil.copytree(skill_dir, target_dir)
169
+ rel = str(target_dir.relative_to(root))
170
+ created.append(rel)
171
+ print(f" \033[32m✓\033[0m Updated {rel}")
172
+
173
+ return created
174
+
175
+
176
+ def generate_context() -> str:
177
+ """Generate the universal Tina4 context document for any AI assistant."""
178
+ return f"""# Tina4 Python — AI Context
179
+
180
+ This project uses **Tina4 Python**, a lightweight, batteries-included web framework
181
+ with zero third-party dependencies for core features.
182
+
183
+ **Documentation:** https://tina4.com
184
+
185
+ ## Quick Start
186
+
187
+ ```bash
188
+ tina4python init . # Scaffold project
189
+ tina4python serve # Start dev server on port 7145
190
+ tina4python migrate # Run database migrations
191
+ tina4python test # Run test suite
192
+ tina4python routes # List all registered routes
193
+ ```
194
+
195
+ ## Project Structure
196
+
197
+ ```
198
+ src/routes/ — Route handlers (auto-discovered, one per resource)
199
+ src/orm/ — ORM models (one per file, filename = class name)
200
+ src/templates/ — Twig/Jinja2 templates (extends base.twig)
201
+ src/app/ — Shared helpers and service classes
202
+ src/scss/ — SCSS files (auto-compiled to public/css/)
203
+ src/public/ — Static assets served at /
204
+ src/locales/ — Translation JSON files
205
+ src/seeds/ — Database seeder scripts
206
+ migrations/ — SQL migration files (sequential numbered)
207
+ tests/ — pytest test files
208
+ ```
209
+
210
+ ## Built-in Features (No External Packages Needed)
211
+
212
+ | Feature | Module | Import |
213
+ |---------|--------|--------|
214
+ | Routing | router | `from tina4_python.core.router import get, post, put, delete` |
215
+ | ORM | orm | `from tina4_python.orm import ORM, IntegerField, StringField` |
216
+ | Database | database | `from tina4_python.database import Database` |
217
+ | Templates | template | `response.render("page.twig", data)` |
218
+ | JWT Auth | auth | `from tina4_python.auth import Auth, hash_password, check_password` |
219
+ | REST API Client | api | `from tina4_python.api import Api` |
220
+ | GraphQL | graphql | `from tina4_python.graphql import GraphQL, Schema` |
221
+ | WebSocket | websocket | `from tina4_python.websocket import WebSocketServer` |
222
+ | SOAP/WSDL | wsdl | `from tina4_python.wsdl import WSDL, wsdl_operation` |
223
+ | Email (SMTP+IMAP) | messenger | `from tina4_python.messenger import Messenger` |
224
+ | Background Queue | queue | `from tina4_python.queue import Queue` |
225
+ | SCSS Compilation | scss | Auto-compiled from src/scss/ |
226
+ | Migrations | migration | `tina4python migrate` CLI command |
227
+ | Seeder | seeder | `from tina4_python.seeder import FakeData, seed_table` |
228
+ | i18n | localization | `from tina4_python.localization import Localization` |
229
+ | Swagger/OpenAPI | swagger | Auto-generated at /swagger |
230
+ | Sessions | session | `request.session.get(key)` / `.set(key, value)` |
231
+ | Middleware | middleware | `@middleware(MyMiddleware)` decorator |
232
+ | HTML Builder | html_element | `from tina4_python.html_element import HTMLElement` |
233
+ | Form Tokens | template | `{{{{ form_token() }}}}` in Twig |
234
+
235
+ ## Key Conventions
236
+
237
+ 1. **Routes return `response()`** — always use `response(data)` not `response.json()`
238
+ 2. **GET routes are public**, POST/PUT/PATCH/DELETE require auth by default
239
+ 3. **Use `@noauth()`** to make write routes public, `@secured()` to protect GET routes
240
+ 4. **Decorator order**: `@noauth/@secured` → `@description/@tags` → `@get/@post` (route decorator innermost)
241
+ 5. **Every template extends `base.twig`** — no standalone HTML pages
242
+ 6. **No inline styles** — use SCSS in `src/scss/` with CSS variables
243
+ 7. **No hardcoded colors** — use `var(--primary)`, `var(--text)`, etc.
244
+ 8. **All schema changes via migrations** — never create tables in route code
245
+ 9. **Service pattern** — complex logic goes in `src/app/` service classes, routes stay thin
246
+ 10. **Use built-in features** — never install packages for things Tina4 already provides
247
+
248
+ ## AI Workflow — Available Skills
249
+
250
+ When using an AI coding assistant with Tina4, these skills are available:
251
+
252
+ | Skill | Description |
253
+ |-------|-------------|
254
+ | `/tina4-route` | Create a new route with proper decorators and auth |
255
+ | `/tina4-orm` | Create an ORM model with migration |
256
+ | `/tina4-crud` | Generate complete CRUD (migration, ORM, routes, template, tests) |
257
+ | `/tina4-auth` | Set up JWT authentication with login/register |
258
+ | `/tina4-api` | Create an external API integration |
259
+ | `/tina4-queue` | Set up background job processing |
260
+ | `/tina4-template` | Create a server-rendered template page |
261
+ | `/tina4-graphql` | Set up a GraphQL endpoint |
262
+ | `/tina4-websocket` | Set up WebSocket communication |
263
+ | `/tina4-wsdl` | Create a SOAP/WSDL service |
264
+ | `/tina4-messenger` | Set up email send/receive |
265
+ | `/tina4-test` | Write tests for a feature |
266
+ | `/tina4-migration` | Create a database migration |
267
+ | `/tina4-seed` | Generate fake data for development |
268
+ | `/tina4-i18n` | Set up internationalization |
269
+ | `/tina4-scss` | Set up SCSS stylesheets |
270
+ | `/tina4-frontend` | Set up a frontend framework |
271
+
272
+ ## Common Patterns
273
+
274
+ ### Route
275
+ ```python
276
+ from tina4_python.core.router import get, post, noauth
277
+ from tina4_python.swagger import description, tags
278
+
279
+ @noauth()
280
+ @description("Create a widget")
281
+ @tags(["widgets"])
282
+ @post("/api/widgets")
283
+ async def create_widget(request, response):
284
+ data = request.body
285
+ return response({{"created": True}}, 201)
286
+ ```
287
+
288
+ ### ORM Model
289
+ ```python
290
+ from tina4_python.orm import ORM, IntegerField, StringField
291
+
292
+ class Widget(ORM):
293
+ id = IntegerField(primary_key=True, auto_increment=True)
294
+ name = StringField()
295
+ ```
296
+
297
+ ### Template
298
+ ```twig
299
+ {{% extends "base.twig" %}}
300
+ {{% block content %}}
301
+ <div class="container">
302
+ <h1>{{{{ title }}}}</h1>
303
+ {{% for item in items %}}
304
+ <p>{{{{ item.name }}}}</p>
305
+ {{% endfor %}}
306
+ </div>
307
+ {{% endblock %}}
308
+ ```
309
+ """
310
+
311
+
312
+ __all__ = ["AI_TOOLS", "is_installed", "show_menu", "install_selected", "install_all", "generate_context"]
@@ -14,7 +14,7 @@ No PyJWT, no cryptography package.
14
14
  payload = auth.valid_token(token)
15
15
 
16
16
  hashed = Auth.hash_password("secret123")
17
- Auth.check_password(hashed, "secret123") # True
17
+ Auth.check_password("secret123", hashed) # True
18
18
  """
19
19
  import os
20
20
  import hmac
@@ -152,6 +152,25 @@ class Auth:
152
152
  auth = cls(secret=secret, expires_in=expires_in)
153
153
  return auth.refresh_token(token)
154
154
 
155
+ @classmethod
156
+ def authenticate_request_static(cls, headers: dict) -> dict | None:
157
+ """Extract and validate auth from request headers without instantiating Auth.
158
+
159
+ Reads SECRET from env. Checks: Bearer JWT, Bearer API key, Basic auth.
160
+ Returns payload dict on success, None on failure.
161
+ """
162
+ secret = os.environ.get("SECRET", "tina4-default-secret")
163
+ auth = cls(secret=secret)
164
+ return auth.authenticate_request(headers)
165
+
166
+ @staticmethod
167
+ def validate_api_key_static(provided: str, expected: str = None) -> bool:
168
+ """Validate an API key without instantiating Auth.
169
+
170
+ Alias for validate_api_key (already a staticmethod).
171
+ """
172
+ return Auth.validate_api_key(provided, expected)
173
+
155
174
  # ── Password Hashing ──────────────────────────────────────────
156
175
 
157
176
  @staticmethod
@@ -164,7 +183,7 @@ class Auth:
164
183
  return f"pbkdf2_sha256${iterations}${salt}${dk.hex()}"
165
184
 
166
185
  @staticmethod
167
- def check_password(hashed: str, password: str) -> bool:
186
+ def check_password(password: str, hashed: str) -> bool:
168
187
  """Verify a password against its PBKDF2 hash."""
169
188
  try:
170
189
  parts = hashed.split("$")
@@ -183,9 +202,18 @@ class Auth:
183
202
  # ── API Key Auth ──────────────────────────────────────────────
184
203
 
185
204
  @staticmethod
186
- def validate_api_key(provided: str) -> bool:
187
- """Check a Bearer token against the TINA4_API_KEY env var (falls back to API_KEY)."""
188
- expected = os.environ.get("TINA4_API_KEY", os.environ.get("API_KEY", ""))
205
+ def validate_api_key(provided: str, expected: str = None) -> bool:
206
+ """Check an API key against an expected value.
207
+
208
+ Args:
209
+ provided: The API key provided in the request.
210
+ expected: The expected API key. If None, reads from
211
+ TINA4_API_KEY env var (falls back to API_KEY).
212
+
213
+ Returns: True if the provided key matches.
214
+ """
215
+ if expected is None:
216
+ expected = os.environ.get("TINA4_API_KEY", os.environ.get("API_KEY", ""))
189
217
  if not expected:
190
218
  return False
191
219
  return hmac.compare_digest(provided, expected)
@@ -255,4 +283,17 @@ def refresh_token(token: str, expires_in: int = 60) -> str | None:
255
283
  return Auth.refresh_token_static(token, expires_in=expires_in)
256
284
 
257
285
 
258
- __all__ = ["Auth", "get_token", "valid_token", "get_payload", "refresh_token"]
286
+ def authenticate_request(headers: dict) -> dict | None:
287
+ """Validate auth from request headers — reads SECRET from env."""
288
+ return Auth.authenticate_request_static(headers)
289
+
290
+
291
+ def validate_api_key(provided: str, expected: str = None) -> bool:
292
+ """Validate an API key. Shortcut for Auth.validate_api_key()."""
293
+ return Auth.validate_api_key(provided, expected)
294
+
295
+
296
+ __all__ = [
297
+ "Auth", "get_token", "valid_token", "get_payload", "refresh_token",
298
+ "authenticate_request", "validate_api_key",
299
+ ]
@@ -363,7 +363,7 @@ def _routes(args):
363
363
  importlib.import_module("app")
364
364
 
365
365
  from tina4_python.core.router import Router
366
- routes = Router.all()
366
+ routes = Router.get_routes()
367
367
  if not routes:
368
368
  print("No routes registered.")
369
369
  return
@@ -405,42 +405,19 @@ def _build(args):
405
405
 
406
406
 
407
407
  def _ai(args):
408
- """Detect AI coding tools and install Tina4 context."""
409
- from tina4_python.ai import detect_ai, install_context, install_all, status_report
408
+ """Install AI coding assistant context files."""
409
+ from tina4_python.ai import show_menu, install_selected, install_all
410
410
 
411
411
  root = "."
412
412
 
413
- if "--all" in args:
414
- # Install context for ALL known AI tools
415
- created = install_all(root, force="--force" in args)
416
- if created:
417
- print("Installed Tina4 context for all AI tools:")
418
- for f in created:
419
- print(f" + {f}")
420
- else:
421
- print("All AI context files already exist. Use --force to overwrite.")
422
- elif "--status" in args or not args:
423
- # Show detection status
424
- print(status_report(root))
425
-
426
- # Auto-install for detected tools
427
- detected = [t for t in detect_ai(root) if t["installed"]]
428
- if detected:
429
- created = install_context(root)
430
- if created:
431
- print("Installed Tina4 context:")
432
- for f in created:
433
- print(f" + {f}")
434
- else:
435
- print("Context files already exist. Use --force to overwrite.")
413
+ if args and args[0].lower() == "all":
414
+ # Non-interactive: install everything
415
+ install_all(root)
436
416
  else:
437
- # Install for specific tool(s)
438
- created = install_context(root, tools=args, force="--force" in args)
439
- if created:
440
- for f in created:
441
- print(f" + {f}")
442
- else:
443
- print("Nothing to install.")
417
+ # Interactive: show menu, get selection
418
+ selection = show_menu(root)
419
+ if selection:
420
+ install_selected(root, selection)
444
421
 
445
422
 
446
423
  def _generate(args):
@@ -231,8 +231,13 @@ class Router:
231
231
  return None, {}
232
232
 
233
233
  @staticmethod
234
- def all() -> list[dict]:
235
- """Return all registered routes (for CLI listing and Swagger)."""
234
+ def get_routes() -> list[dict]:
235
+ """Return all registered routes."""
236
+ return _routes
237
+
238
+ @staticmethod
239
+ def list_routes() -> list[dict]:
240
+ """Return all registered routes (debug-friendly)."""
236
241
  return _routes
237
242
 
238
243
  @staticmethod
@@ -757,7 +757,7 @@ async def app(scope: dict, receive, send):
757
757
  # Serve OpenAPI spec JSON from all registered routes
758
758
  from tina4_python.swagger import Swagger as _SwaggerGen
759
759
  _swagger = _SwaggerGen()
760
- _spec = _swagger.generate(Router.all())
760
+ _spec = _swagger.generate(Router.get_routes())
761
761
  response.json(_spec)
762
762
  _cors.apply(request, response)
763
763
  headers = response.build_headers("")
@@ -929,7 +929,7 @@ async def app(scope: dict, receive, send):
929
929
  matched_pattern = route["path"] if route else "-"
930
930
  toolbar = render_dev_toolbar(
931
931
  request.method, request.path, matched_pattern,
932
- request_id, len(Router.all()),
932
+ request_id, len(Router.get_routes()),
933
933
  ).encode()
934
934
  content = response.content
935
935
  # Inject before </body> if present, else append
@@ -1216,7 +1216,7 @@ def run(host: str | None = None, port: int | None = None):
1216
1216
 
1217
1217
  # Auto-discover routes
1218
1218
  _auto_discover("src")
1219
- route_count = len(Router.all())
1219
+ route_count = len(Router.get_routes())
1220
1220
  Log.info(f"Discovered {route_count} routes")
1221
1221
 
1222
1222
  # Resolve host/port (CLI arg > ENV > default)