cherrypy-foundation 1.0.0a11__tar.gz → 1.0.0a13__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 (153) hide show
  1. {cherrypy_foundation-1.0.0a11/src/cherrypy_foundation.egg-info → cherrypy_foundation-1.0.0a13}/PKG-INFO +1 -1
  2. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/ldap.py +1 -1
  3. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_ldap.py +75 -2
  4. cherrypy_foundation-1.0.0a13/src/cherrypy_foundation/tools/i18n.py +495 -0
  5. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_i18n.py +9 -2
  6. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13/src/cherrypy_foundation.egg-info}/PKG-INFO +1 -1
  7. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/SOURCES.txt +0 -2
  8. cherrypy_foundation-1.0.0a11/src/cherrypy_foundation/tools/i18n.py +0 -454
  9. cherrypy_foundation-1.0.0a11/src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  10. cherrypy_foundation-1.0.0a11/src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po +0 -15
  11. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/.gitignore +0 -0
  12. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/.gitlab-ci.yml +0 -0
  13. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/LICENSE.md +0 -0
  14. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/README.md +0 -0
  15. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/TODO +0 -0
  16. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/changelog +0 -0
  17. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/control +0 -0
  18. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/copyright +0 -0
  19. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/python3-cherrypy-foundation.docs +0 -0
  20. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/python3-cherrypy-foundation.links +0 -0
  21. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/rules +0 -0
  22. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/source/format +0 -0
  23. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/source/options +0 -0
  24. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/debian/upstream/metadata +0 -0
  25. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/pyproject.toml +0 -0
  26. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/setup.cfg +0 -0
  27. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/sonar-project.properties +0 -0
  28. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/__init__.py +0 -0
  29. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/ColorModes.jinja +0 -0
  30. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Datatable.css +0 -0
  31. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Datatable.jinja +0 -0
  32. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Datatable.js +0 -0
  33. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Field.css +0 -0
  34. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Field.jinja +0 -0
  35. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Field.js +0 -0
  36. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Fields.jinja +0 -0
  37. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Flash.jinja +0 -0
  38. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Icon.jinja +0 -0
  39. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/SideBySideMultiSelect.css +0 -0
  40. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/SideBySideMultiSelect.jinja +0 -0
  41. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/SideBySideMultiSelect.js +0 -0
  42. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Typeahead.css +0 -0
  43. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Typeahead.jinja +0 -0
  44. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/Typeahead.js +0 -0
  45. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/__init__.py +0 -0
  46. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/tests/__init__.py +0 -0
  47. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/tests/test_static.py +0 -0
  48. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +0 -0
  49. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +0 -0
  50. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  51. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  52. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +0 -0
  53. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +0 -0
  54. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +0 -0
  55. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +0 -0
  56. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +0 -0
  57. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +0 -0
  58. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +0 -0
  59. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +0 -0
  60. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +0 -0
  61. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +0 -0
  62. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +0 -0
  63. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
  64. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
  65. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
  66. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
  67. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
  68. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/js/dataTables.js +0 -0
  69. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +0 -0
  70. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +0 -0
  71. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +0 -0
  72. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +0 -0
  73. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +0 -0
  74. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +0 -0
  75. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +0 -0
  76. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +0 -0
  77. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +0 -0
  78. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +0 -0
  79. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +0 -0
  80. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +0 -0
  81. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +0 -0
  82. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +0 -0
  83. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +0 -0
  84. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +0 -0
  85. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +0 -0
  86. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +0 -0
  87. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +0 -0
  88. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +0 -0
  89. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +0 -0
  90. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +0 -0
  91. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +0 -0
  92. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +0 -0
  93. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/jquery/jquery.min.js +0 -0
  94. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/LICENSE +0 -0
  95. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/README.md +0 -0
  96. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/multi.css +0 -0
  97. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/multi/multi.js +0 -0
  98. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/popper/popper.js +0 -0
  99. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/popper/popper.min.js +0 -0
  100. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +0 -0
  101. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +0 -0
  102. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/error_page.py +0 -0
  103. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/flash.py +0 -0
  104. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/form.py +0 -0
  105. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/logging.py +0 -0
  106. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/passwd.py +0 -0
  107. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/__init__.py +0 -0
  108. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/db.py +0 -0
  109. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/restapi.py +0 -0
  110. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/scheduler.py +0 -0
  111. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/smtp.py +0 -0
  112. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/__init__.py +0 -0
  113. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_db.py +0 -0
  114. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_scheduler.py +0 -0
  115. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -0
  116. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/plugins/tests/test_smtp.py +0 -0
  117. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/__init__.py +0 -0
  118. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/templates/test_flash.html +0 -0
  119. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/templates/test_form.html +0 -0
  120. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/templates/test_url.html +0 -0
  121. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_error_page.py +0 -0
  122. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_flash.py +0 -0
  123. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_form.py +0 -0
  124. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_passwd.py +0 -0
  125. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tests/test_url.py +0 -0
  126. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/__init__.py +0 -0
  127. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/auth.py +0 -0
  128. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/auth_mfa.py +0 -0
  129. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/errors.py +0 -0
  130. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/jinja2.py +0 -0
  131. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/ratelimit.py +0 -0
  132. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/secure_headers.py +0 -0
  133. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/sessions_timeout.py +0 -0
  134. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/__init__.py +0 -0
  135. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/components/Button.jinja +0 -0
  136. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  137. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +0 -0
  138. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
  139. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +0 -0
  140. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/locales/messages.pot +0 -0
  141. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -0
  142. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +0 -0
  143. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -0
  144. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_auth.py +0 -0
  145. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -0
  146. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_jinja2.py +0 -0
  147. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/tools/tests/test_ratelimit.py +0 -0
  148. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/url.py +0 -0
  149. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation/widgets.py +0 -0
  150. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/dependency_links.txt +0 -0
  151. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/requires.txt +0 -0
  152. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/src/cherrypy_foundation.egg-info/top_level.txt +0 -0
  153. {cherrypy_foundation-1.0.0a11 → cherrypy_foundation-1.0.0a13}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cherrypy-foundation
3
- Version: 1.0.0a11
3
+ Version: 1.0.0a13
4
4
  Summary: CherryPy Foundation
5
5
  Author-email: Patrik Dufresne <patrik@ikus-soft.com>
6
6
  License: GPLv3
@@ -63,7 +63,7 @@ def first_attribute(attributes, keys, default=None):
63
63
  value = attributes[attr]
64
64
  if isinstance(value, list):
65
65
  if len(value) == 0:
66
- pass
66
+ continue
67
67
  return value[0]
68
68
  else:
69
69
  return value
@@ -19,13 +19,13 @@ Created on Oct 17, 2015
19
19
  @author: Patrik Dufresne <patrik@ikus-soft.com>
20
20
  """
21
21
  import os
22
- from unittest import mock, skipUnless
22
+ from unittest import TestCase, mock, skipUnless
23
23
 
24
24
  import cherrypy
25
25
  import ldap3
26
26
  from cherrypy.test import helper
27
27
 
28
- from .. import ldap # noqa
28
+ from ..ldap import all_attribute, first_attribute # noqa
29
29
 
30
30
  original_connection = ldap3.Connection
31
31
 
@@ -35,6 +35,79 @@ def mock_ldap_connection(*args, **kwargs):
35
35
  return original_connection(*args, client_strategy=ldap3.MOCK_ASYNC, **kwargs)
36
36
 
37
37
 
38
+ class LdapFirstAttributeTest(TestCase):
39
+
40
+ def test_no_keys_returns_default(self):
41
+ attributes = {"cn": ["John Doe"]}
42
+ self.assertIsNone(first_attribute(attributes, None))
43
+ self.assertEqual(first_attribute(attributes, [], default="fallback"), "fallback")
44
+
45
+ def test_single_key_with_scalar_value(self):
46
+ attributes = {"uid": "jdoe"}
47
+ self.assertEqual(first_attribute(attributes, "uid"), "jdoe")
48
+
49
+ def test_single_key_with_list_value(self):
50
+ attributes = {"mail": ["john@example.com", "alt@example.com"]}
51
+ self.assertEqual(first_attribute(attributes, "mail"), "john@example.com")
52
+
53
+ def test_empty_list_value_is_skipped(self):
54
+ attributes = {
55
+ "mail": [],
56
+ "uid": ["jdoe"],
57
+ }
58
+ self.assertEqual(first_attribute(attributes, ["mail", "uid"]), "jdoe")
59
+
60
+ def test_missing_first_key_uses_next_key(self):
61
+ attributes = {"cn": ["John Doe"]}
62
+ self.assertEqual(first_attribute(attributes, ["sn", "cn"]), "John Doe")
63
+
64
+ def test_all_keys_missing_returns_default(self):
65
+ attributes = {"cn": ["John Doe"]}
66
+ self.assertEqual(first_attribute(attributes, ["sn", "uid"], default="unknown"), "unknown")
67
+
68
+ def test_key_not_found_without_default_returns_none(self):
69
+ attributes = {"cn": ["John Doe"]}
70
+ self.assertIsNone(first_attribute(attributes, "uid"))
71
+
72
+
73
+ class LdapAllAttributeTest(TestCase):
74
+
75
+ def test_no_keys_returns_default(self):
76
+ attributes = {"cn": ["John Doe"]}
77
+ self.assertIsNone(all_attribute(attributes, None))
78
+ self.assertEqual(all_attribute(attributes, [], default="fallback"), "fallback")
79
+
80
+ def test_single_key_with_scalar_value(self):
81
+ attributes = {"uid": "jdoe"}
82
+ self.assertEqual(all_attribute(attributes, "uid"), ["jdoe"])
83
+
84
+ def test_single_key_with_list_value(self):
85
+ attributes = {"mail": ["john@example.com", "alt@example.com"]}
86
+ self.assertEqual(all_attribute(attributes, "mail"), ["john@example.com"])
87
+
88
+ def test_multiple_keys_collect_values(self):
89
+ attributes = {
90
+ "uid": "jdoe",
91
+ "cn": ["John Doe"],
92
+ }
93
+ self.assertEqual(all_attribute(attributes, ["uid", "cn"]), ["jdoe", "John Doe"])
94
+
95
+ def test_missing_keys_are_skipped(self):
96
+ attributes = {"cn": ["John Doe"]}
97
+ self.assertEqual(all_attribute(attributes, ["sn", "cn", "uid"]), ["John Doe"])
98
+
99
+ def test_all_keys_missing_returns_default(self):
100
+ attributes = {"cn": ["John Doe"]}
101
+ self.assertEqual(all_attribute(attributes, ["sn", "uid"], default=[]), [])
102
+
103
+ def test_empty_list_value_is_appended_as_empty_list(self):
104
+ attributes = {
105
+ "mail": [],
106
+ "uid": "jdoe",
107
+ }
108
+ self.assertEqual(all_attribute(attributes, ["mail", "uid"]), [[], "jdoe"])
109
+
110
+
38
111
  class LdapPluginTest(helper.CPWebCase):
39
112
 
40
113
  @classmethod
@@ -0,0 +1,495 @@
1
+ # Internationalisation tool for cherrypy
2
+ # Copyright (C) 2012-2025 Patrik Dufresne
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ """
18
+ Internationalization (i18n) and Localization (l10n) support for CherryPy.
19
+
20
+ This module provides a CherryPy tool that integrates GNU gettext and Babel
21
+ to handle language selection, translations, locale-aware formatting, and
22
+ timezone handling on a per-request basis.
23
+
24
+ The active language is resolved in the following order (highest priority first):
25
+
26
+ 1. Language explicitly set with ``with i18n.preferred_lang():``
27
+ 2. User-defined callback (``tools.i18n.func``)
28
+ 3. HTTP ``Accept-Language`` request header
29
+ 4. Default language configured via ``tools.i18n.default``
30
+
31
+ Translations are loaded using Babel and gettext, and the resolved locale is
32
+ available through ``i18n.get_translation()`` during request handling.
33
+
34
+ ---------------------------------------------------------------------------
35
+ Basic usage
36
+ ---------------------------------------------------------------------------
37
+
38
+ Within Python code, mark translatable strings using ``ugettext`` or
39
+ ``ungettext``:
40
+
41
+ from i18n import gettext as _, ngettext
42
+
43
+ class MyController:
44
+ @cherrypy.expose
45
+ def index(self):
46
+ locale = cherrypy.response.i18n.locale
47
+ s1 = _(u"Translatable string")
48
+ s2 = ngettext(
49
+ u"There is one item.",
50
+ u"There are multiple items.",
51
+ 2
52
+ )
53
+ return "<br />".join([s1, s2, locale.display_name])
54
+
55
+ ---------------------------------------------------------------------------
56
+ Lazy translations
57
+ ---------------------------------------------------------------------------
58
+
59
+ If code is executed before a CherryPy response object is available
60
+ (e.g. model definitions or module-level constants), use the ``*_lazy``
61
+ helpers. These defer translation until the value is actually rendered:
62
+
63
+ from i18n_tool import gettext_lazy
64
+
65
+ class Model:
66
+ name = gettext_lazy(u"Model name")
67
+
68
+ ---------------------------------------------------------------------------
69
+ Templates
70
+ ---------------------------------------------------------------------------
71
+
72
+ For template rendering, i18n integrate with jinja2.
73
+
74
+ {% trans %}Text to translate{% endtrans %}
75
+ {{ _('Text to translate') }}
76
+ {{ get_translation().gettext('Text to translate') }}
77
+ {{ get_translation().locale }}
78
+ {{ var | format_datetime(format='full') }}
79
+ {{ var | format_date(format='full') }}
80
+
81
+ ---------------------------------------------------------------------------
82
+ Configuration
83
+ ---------------------------------------------------------------------------
84
+
85
+ Example CherryPy configuration:
86
+
87
+ [/]
88
+ tools.i18n.on = True
89
+ tools.i18n.default = "en_US"
90
+ tools.i18n.mo_dir = "/path/to/i18n"
91
+ tools.i18n.domain = "myapp"
92
+
93
+ The ``mo_dir`` directory must contain subdirectories named after language
94
+ codes (e.g. ``en``, ``fr_CA``), each containing an ``LC_MESSAGES`` directory
95
+ with the compiled ``.mo`` file:
96
+
97
+ <mo_dir>/<language>/LC_MESSAGES/<domain>.mo
98
+
99
+ """
100
+
101
+ import logging
102
+ import os
103
+ from contextlib import contextmanager
104
+ from contextvars import ContextVar
105
+ from functools import lru_cache
106
+ from gettext import NullTranslations, translation
107
+
108
+ import cherrypy
109
+ import pytz
110
+ from babel import dates
111
+ from babel.core import Locale, get_global
112
+ from babel.support import LazyProxy, Translations
113
+
114
+ _preferred_lang = ContextVar('preferred_lang', default=())
115
+ _preferred_timezone = ContextVar('preferred_timezone', default=())
116
+ _translation = ContextVar('translation', default=None)
117
+ _tzinfo = ContextVar('tzinfo', default=None)
118
+
119
+
120
+ def _get_config(key, default=None):
121
+ """
122
+ Lookup configuration from request, if available. Fallback to global config.
123
+ """
124
+ if getattr(cherrypy, 'request') and getattr(cherrypy.request, 'config') and key in cherrypy.request.config:
125
+ return cherrypy.request.config[key]
126
+ return cherrypy.config.get(key, default)
127
+
128
+
129
+ @contextmanager
130
+ def preferred_lang(lang):
131
+ """
132
+ Re-define the preferred language to be used for translation within a given context.
133
+
134
+ with i18n.preferred_lang('fr'):
135
+ i18n.gettext('some string')
136
+ """
137
+ if not (lang is None or isinstance(lang, str)):
138
+ raise ValueError(lang)
139
+ try:
140
+ # Update preferred lang and clear translation.
141
+ if lang:
142
+ token_l = _preferred_lang.set((lang,))
143
+ else:
144
+ token_l = _preferred_lang.set(tuple())
145
+ token_t = _translation.set(None)
146
+ yield
147
+ finally:
148
+ # Restore previous value
149
+ _preferred_lang.reset(token_l)
150
+ _translation.reset(token_t)
151
+
152
+
153
+ @contextmanager
154
+ def preferred_timezone(timezone):
155
+ """
156
+ Re-define the preferred timezone to be used for date format within a given context.
157
+
158
+ with i18n.preferred_lang('America/Montreal'):
159
+ i18n.format_datetime(...)
160
+ """
161
+ if not (timezone is None or isinstance(timezone, str)):
162
+ raise ValueError(timezone)
163
+ try:
164
+ # Update preferred timezone and clear tzinfo.
165
+ if timezone:
166
+ token_t = _preferred_timezone.set((timezone,))
167
+ else:
168
+ token_t = _preferred_timezone.set(tuple())
169
+ token_z = _tzinfo.set(None)
170
+ yield
171
+ finally:
172
+ # Restore previous value
173
+ _preferred_timezone.reset(token_t)
174
+ _tzinfo.reset(token_z)
175
+
176
+
177
+ @lru_cache(maxsize=32)
178
+ def _search_translation(dirname, domain, *locales, sourcecode_lang='en'):
179
+ """
180
+ Loads the first existing translations for known locale.
181
+
182
+ :parameters:
183
+ langs : List
184
+ List of languages as returned by `parse_accept_language_header`.
185
+ dirname : String
186
+ A single directory of the translations (`tools.i18n.mo_dir`).
187
+ domain : String
188
+ Gettext domain of the catalog (`tools.i18n.domain`).
189
+
190
+ :returns: Translations, the corresponding Locale object.
191
+ """
192
+ if not isinstance(locales, (list, tuple)):
193
+ locales = tuple(locales)
194
+
195
+ # Loop on each locales to find the best matching translation.
196
+ for locale in locales:
197
+ try:
198
+ # Use `gettext.translation()` instead of `gettext.find()` to chain translation fr_CA -> fr -> src.
199
+ t = translation(domain=domain, localedir=dirname, languages=[locale], fallback=True, class_=Translations)
200
+ except Exception:
201
+ # If exception occur while loading the translation file. The file is probably corrupted.
202
+ cherrypy.log(
203
+ f'failed to load gettext catalog domain={domain} localedir={dirname} locale={locale}',
204
+ context='I18N',
205
+ severity=logging.WARNING,
206
+ traceback=True,
207
+ )
208
+ continue
209
+ if t.__class__ is NullTranslations and not locale.startswith(sourcecode_lang):
210
+ # Continue searching if translation is not found.
211
+ continue
212
+ t.locale = Locale.parse(locale)
213
+ return t
214
+ # If translation file not found, return default
215
+ t = NullTranslations()
216
+ t.locale = Locale(sourcecode_lang)
217
+ return t
218
+
219
+
220
+ def get_language_name(lang_code):
221
+ """
222
+ Translate the language code into it's language display name.
223
+ """
224
+ try:
225
+ locale = Locale.parse(lang_code)
226
+ except Exception:
227
+ return lang_code
228
+ translation = get_translation()
229
+ return locale.get_language_name(translation.locale)
230
+
231
+
232
+ def get_timezone():
233
+ """
234
+ Get the best timezone information for the current context.
235
+
236
+ The timezone returned is determined with the following priorities:
237
+
238
+ * value of preferred_timezone()
239
+ * tools.i18n.default_timezone
240
+ * default server time.
241
+
242
+ """
243
+ # When tzinfo is defined, use it
244
+ tzinfo = _tzinfo.get()
245
+ if tzinfo is not None:
246
+ return tzinfo
247
+ # Otherwise search for a valid timezone.
248
+ default = _get_config('tools.i18n.default_timezone')
249
+ preferred_timezone = _preferred_timezone.get()
250
+ if default and default not in preferred_timezone:
251
+ preferred_timezone = (
252
+ *preferred_timezone,
253
+ default,
254
+ )
255
+ for timezone in preferred_timezone:
256
+ try:
257
+ tzinfo = dates.get_timezone(timezone)
258
+ break
259
+ except Exception:
260
+ pass
261
+ # If we can't find a valid timezone using the default and preferred value, fall back to server timezone.
262
+ if tzinfo is None:
263
+ tzinfo = dates.get_timezone(None)
264
+ _tzinfo.set(tzinfo)
265
+ return tzinfo
266
+
267
+
268
+ def get_translation():
269
+ """
270
+ Get the best translation for the current context.
271
+ """
272
+ # When translation is defined, use it
273
+ translation = _translation.get()
274
+ if translation is not None:
275
+ return translation
276
+
277
+ # Otherwise, we need to search the translation.
278
+ # `preferred_lang` should always has a sane value within a cherrypy request because of hooks
279
+ # But we also need to support calls outside cherrypy.
280
+ sourcecode_lang = _get_config('tools.i18n.sourcecode_lang', 'en')
281
+ default = _get_config('tools.i18n.default')
282
+ preferred_lang = _preferred_lang.get()
283
+ if default and default not in preferred_lang:
284
+ preferred_lang = (
285
+ *preferred_lang,
286
+ default,
287
+ )
288
+ mo_dir = _get_config('tools.i18n.mo_dir')
289
+ domain = _get_config('tools.i18n.domain', 'messages')
290
+ translation = _search_translation(mo_dir, domain, *preferred_lang, sourcecode_lang=sourcecode_lang)
291
+ _translation.set(translation)
292
+ return translation
293
+
294
+
295
+ def list_available_locales():
296
+ """
297
+ Return a list of available translations.
298
+ """
299
+ mo_dir = _get_config('tools.i18n.mo_dir')
300
+ domain = _get_config('tools.i18n.domain', 'messages')
301
+ if not mo_dir:
302
+ return
303
+ for lang in os.listdir(mo_dir):
304
+ if os.path.exists(os.path.join(mo_dir, lang, 'LC_MESSAGES', f'{domain}.mo')):
305
+ try:
306
+ yield Locale.parse(lang)
307
+ except Exception:
308
+ continue
309
+
310
+
311
+ def list_available_timezones():
312
+ """
313
+ Return list of available timezone.
314
+ """
315
+ # Babel only support a narrow list of timezone.
316
+ babel_timezone = get_global('zone_territories').keys()
317
+ return [t for t in pytz.all_timezones if t in babel_timezone]
318
+
319
+
320
+ # Public translation functions
321
+ def gettext(message):
322
+ """Standard translation function. You can use it in all your exposed
323
+ methods and everywhere where the response object is available.
324
+
325
+ :parameters:
326
+ message : Unicode
327
+ The message to translate.
328
+
329
+ :returns: The translated message.
330
+ :rtype: Unicode
331
+ """
332
+ return get_translation().gettext(message)
333
+
334
+
335
+ ugettext = gettext
336
+
337
+
338
+ def ngettext(singular, plural, num):
339
+ """Like ugettext, but considers plural forms.
340
+
341
+ :parameters:
342
+ singular : Unicode
343
+ The message to translate in singular form.
344
+ plural : Unicode
345
+ The message to translate in plural form.
346
+ num : Integer
347
+ Number to apply the plural formula on. If num is 1 or no
348
+ translation is found, singular is returned.
349
+
350
+ :returns: The translated message as singular or plural.
351
+ :rtype: Unicode
352
+ """
353
+ return get_translation().ngettext(singular, plural, num)
354
+
355
+
356
+ ungettext = ngettext
357
+
358
+
359
+ def gettext_lazy(message):
360
+ """Like gettext, but lazy.
361
+
362
+ :returns: A proxy for the translation object.
363
+ :rtype: LazyProxy
364
+ """
365
+
366
+ def func():
367
+ return get_translation().gettext(message)
368
+
369
+ return LazyProxy(func, enable_cache=False)
370
+
371
+
372
+ def format_datetime(datetime=None, format='medium', tzinfo=None):
373
+ """
374
+ Wrapper around babel format_datetime to use current locale and current timezone.
375
+ """
376
+ return dates.format_datetime(
377
+ datetime=datetime,
378
+ format=format,
379
+ locale=get_translation().locale,
380
+ tzinfo=tzinfo or get_timezone(),
381
+ )
382
+
383
+
384
+ def format_date(datetime=None, format='medium', tzinfo=None):
385
+ """
386
+ Wrapper around babel format_date to provide a default locale.
387
+ """
388
+ # To enforce the timezone and locale, make use of format_datetime for dates.
389
+ return dates.format_datetime(
390
+ datetime=datetime,
391
+ format=dates.get_date_format(format),
392
+ locale=get_translation().locale,
393
+ tzinfo=tzinfo or get_timezone(),
394
+ )
395
+
396
+
397
+ def get_timezone_name(tzinfo, width='long'):
398
+ return dates.get_timezone_name(tzinfo, width=width, locale=get_translation().locale)
399
+
400
+
401
+ def _load_default(mo_dir, domain, default, **kwargs):
402
+ """
403
+ Initialize the language using the default value from the configuration.
404
+ """
405
+ # Clear current translation
406
+ _preferred_lang.set(tuple())
407
+ _preferred_timezone.set(tuple())
408
+ cherrypy.request._i18n_lang_func = kwargs.get('lang', kwargs.get('func', False))
409
+ cherrypy.request._i18n_tzinfo_func = kwargs.get('tzinfo', False)
410
+ # Clear current translation
411
+ _translation.set(None)
412
+ # Clear current timezone
413
+ _tzinfo.set(None)
414
+
415
+
416
+ def _load_accept_language(**kwargs):
417
+ """
418
+ When running within a request, load the preferred language from Accept-Language header.
419
+ """
420
+ if cherrypy.request.headers.elements('Accept-Language'):
421
+ # Sort language by quality
422
+ languages = sorted(cherrypy.request.headers.elements('Accept-Language'), key=lambda x: x.qvalue, reverse=True)
423
+ _preferred_lang.set(tuple(lang.value.replace('-', '_') for lang in languages))
424
+ # Clear current translation
425
+ _translation.set(None)
426
+
427
+
428
+ def _load_func_language(**kwargs):
429
+ """
430
+ When running a request where a current user is found, load preferred language from user preferences.
431
+ """
432
+ func = getattr(cherrypy.request, '_i18n_lang_func', False)
433
+ if not func:
434
+ return
435
+ try:
436
+ lang = func()
437
+ except Exception:
438
+ return
439
+ if not lang:
440
+ return
441
+ # Add custom lang to preferred_lang
442
+ _preferred_lang.set((lang,))
443
+ # Clear current translation
444
+ _translation.set(None)
445
+
446
+
447
+ def _load_func_tzinfo(**kwargs):
448
+ """
449
+ When running a request, load the preferred timezone information from user preferences.
450
+ """
451
+ func = getattr(cherrypy.request, '_i18n_tzinfo_func', False)
452
+ if not func:
453
+ return
454
+ try:
455
+ tzinfo = func()
456
+ except Exception:
457
+ return
458
+ if not tzinfo:
459
+ return
460
+ # Add custom lang to preferred_lang
461
+ _preferred_timezone.set((tzinfo,))
462
+ # Clear current translation
463
+ _tzinfo.set(None)
464
+
465
+
466
+ def _set_content_language(**kwargs):
467
+ """
468
+ Sets the Content-Language response header (if not already set) to the
469
+ language of `cherrypy.response.i18n.locale`.
470
+ """
471
+ if 'Content-Language' not in cherrypy.response.headers:
472
+ # Only define the content language if the handler uses i18n module.
473
+ translation = _translation.get()
474
+ if translation:
475
+ locale = translation.locale
476
+ language_tag = f"{locale.language}-{locale.territory}" if locale.territory else locale.language
477
+ cherrypy.response.headers['Content-Language'] = language_tag
478
+
479
+
480
+ class I18nTool(cherrypy.Tool):
481
+ """Tool to integrate babel translations in CherryPy."""
482
+
483
+ def __init__(self):
484
+ super().__init__('before_handler', _load_default, 'i18n')
485
+
486
+ def _setup(self):
487
+ cherrypy.Tool._setup(self)
488
+ # Attach additional hooks as different priority to update preferred lang with more accurate preferences.
489
+ cherrypy.request.hooks.attach('before_handler', _load_accept_language, priority=60)
490
+ cherrypy.request.hooks.attach('before_handler', _load_func_language, priority=75)
491
+ cherrypy.request.hooks.attach('before_handler', _load_func_tzinfo, priority=75)
492
+ cherrypy.request.hooks.attach('before_finalize', _set_content_language)
493
+
494
+
495
+ cherrypy.tools.i18n = I18nTool()
@@ -38,7 +38,6 @@ class TestI18n(unittest.TestCase):
38
38
  def test_search_translation_en(self):
39
39
  # Load default translation return translation
40
40
  t = i18n._search_translation(self.mo_dir, 'messages', 'en')
41
- self.assertIsInstance(t, gettext.GNUTranslations)
42
41
  self.assertEqual("en", t.locale.language)
43
42
  # Test translation object
44
43
  self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
@@ -54,7 +53,8 @@ class TestI18n(unittest.TestCase):
54
53
  def test_search_translation_invalid(self):
55
54
  # Load invalid translation return None
56
55
  t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
57
- self.assertIsNone(t)
56
+ # Return Null translations.
57
+ self.assertIn('NullTranslations', str(t.__class__))
58
58
 
59
59
 
60
60
  class Root:
@@ -117,6 +117,13 @@ class TestI18nWebCase(AbstractI18nTest):
117
117
  self.assertHeaderItemValue("Content-Language", "fr-CA")
118
118
  self.assertInBody(TEXT_FR)
119
119
 
120
+ def test_language_en_US_POSIX(self):
121
+ # When calling with locale variant
122
+ self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
123
+ self.assertStatus('200 OK')
124
+ # Tehn page return en-US
125
+ self.assertHeaderItemValue("Content-Language", "en-US")
126
+
120
127
  def test_with_preferred_lang(self):
121
128
  # Given a default lang 'en'
122
129
  date = datetime.fromtimestamp(1680111611, timezone.utc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cherrypy-foundation
3
- Version: 1.0.0a11
3
+ Version: 1.0.0a13
4
4
  Summary: CherryPy Foundation
5
5
  Author-email: Patrik Dufresne <patrik@ikus-soft.com>
6
6
  License: GPLv3
@@ -141,8 +141,6 @@ src/cherrypy_foundation/tools/tests/components/Button.jinja
141
141
  src/cherrypy_foundation/tools/tests/locales/messages.pot
142
142
  src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo
143
143
  src/cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po
144
- src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo
145
- src/cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.po
146
144
  src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo
147
145
  src/cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po
148
146
  src/cherrypy_foundation/tools/tests/templates/test_jinja2.html