zrb 1.0.0b9__py3-none-any.whl → 1.1.0__py3-none-any.whl

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 (88) hide show
  1. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
  2. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +131 -2
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +128 -5
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +297 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_create_my_entity.py +53 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_delete_my_entity.py +62 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_read_my_entity.py +65 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_update_my_entity.py +61 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +81 -13
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -3
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +8 -1
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +56 -12
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +10 -4
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +136 -52
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +3 -3
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +19 -8
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +46 -43
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +5 -2
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +16 -21
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +277 -44
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +66 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +6 -1
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
  57. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
  58. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
  59. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +15 -0
  60. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
  61. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
  62. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  63. zrb/runner/web_route/static/refresh-token.template.js +9 -0
  64. zrb/runner/web_route/static/static_route.py +1 -1
  65. zrb/task/base_task.py +10 -10
  66. zrb/util/codemod/modification_mode.py +3 -0
  67. zrb/util/codemod/modify_class.py +58 -0
  68. zrb/util/codemod/modify_class_parent.py +68 -0
  69. zrb/util/codemod/modify_class_property.py +128 -0
  70. zrb/util/codemod/modify_dict.py +75 -0
  71. zrb/util/codemod/modify_function.py +65 -0
  72. zrb/util/codemod/modify_function_call.py +68 -0
  73. zrb/util/codemod/modify_method.py +88 -0
  74. zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
  75. zrb/util/file.py +3 -2
  76. zrb/util/load.py +13 -7
  77. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
  78. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/RECORD +80 -46
  79. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
  80. zrb/util/codemod/append_code_to_class.py +0 -35
  81. zrb/util/codemod/append_code_to_function.py +0 -38
  82. zrb/util/codemod/append_code_to_method.py +0 -55
  83. zrb/util/codemod/append_key_to_dict.py +0 -51
  84. zrb/util/codemod/append_param_to_function_call.py +0 -39
  85. zrb/util/codemod/prepend_parent_to_class.py +0 -38
  86. zrb/util/codemod/prepend_property_to_class.py +0 -55
  87. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
  88. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,94 @@
1
+ const CRUD_UTIL = {
2
+
3
+ renderPagination(paginationComponent, crudState, total, fetchFunction = "fetchRows") {
4
+ const totalPages = Math.ceil(total / crudState.pageSize);
5
+ paginationComponent.innerHTML = "";
6
+ // Ensure left alignment (if not already handled by PicoCSS or external CSS)
7
+ paginationComponent.style.textAlign = "left";
8
+ let paginationHTML = "";
9
+ // Only show "First" and "Previous" if we're not on page 1
10
+ if (crudState.currentPage > 1) {
11
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(1)">&laquo;</button>`;
12
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${crudState.currentPage - 1})">&lt;</button>`;
13
+ }
14
+ if (totalPages <= 5) {
15
+ // If total pages are few, simply list them all
16
+ for (let i = 1; i <= totalPages; i++) {
17
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${i})" ${i === crudState.currentPage ? "disabled" : ""}>${i}</button>`;
18
+ }
19
+ } else {
20
+ // Always show first page
21
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(1)" ${crudState.currentPage === 1 ? "disabled" : ""}>1</button>`;
22
+ // Determine start and end for the page range around current page
23
+ const start = Math.max(2, crudState.currentPage - 1);
24
+ const end = Math.min(totalPages - 1, crudState.currentPage + 1);
25
+ // Add ellipsis if there's a gap between first page and the start of the range
26
+ if (start > 2) {
27
+ paginationHTML += `<span style="padding: 0 5px;">...</span>`;
28
+ }
29
+ // Render the range around the current page
30
+ for (let i = start; i <= end; i++) {
31
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${i})" ${i === crudState.currentPage ? "disabled" : ""}>${i}</button>`;
32
+ }
33
+ // Add ellipsis if there's a gap between the end of the range and the last page
34
+ if (end < totalPages - 1) {
35
+ paginationHTML += `<span style="padding: 0 5px;">...</span>`;
36
+ }
37
+ // Always show last page
38
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${totalPages})" ${crudState.currentPage === totalPages ? "disabled" : ""}>${totalPages}</button>`;
39
+ }
40
+ // Only show "Next" and "Last" if we're not on the last page
41
+ if (crudState.currentPage < totalPages) {
42
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${crudState.currentPage + 1})">&gt;</button>`;
43
+ paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${totalPages})">&raquo;</button>`;
44
+ }
45
+ paginationComponent.innerHTML = paginationHTML;
46
+ },
47
+
48
+ splitUnescaped(query, delimiter=",") {
49
+ const parts = [];
50
+ let current = "";
51
+ let escaped = false;
52
+ for (let i = 0; i < query.length; i++) {
53
+ const char = query[i];
54
+ if (escaped) {
55
+ current += char;
56
+ escaped = false;
57
+ } else if (char === "\\") {
58
+ escaped = true;
59
+ } else if (char === delimiter) {
60
+ parts.push(current);
61
+ current = "";
62
+ } else {
63
+ current += char;
64
+ }
65
+ }
66
+ if (current != "") {
67
+ parts.push(current);
68
+ }
69
+ return parts;
70
+ },
71
+
72
+ isValidFilterQuery(query) {
73
+ const filterPattern = /^([\w]+):(eq|ne|gt|gte|lt|lte|like|in):(.+)$/;
74
+ const parts = this.splitUnescaped(query);
75
+ return parts.every(part => filterPattern.test(part));
76
+ },
77
+
78
+ getSearchParam(crudState, defaultSearchColumn, apiMode = false) {
79
+ return new URLSearchParams({
80
+ page: crudState.currentPage || 1,
81
+ page_size: crudState.pageSize || 10,
82
+ filter: this._getFilterSearchParamValue(crudState, defaultSearchColumn, apiMode),
83
+ }).toString();
84
+ },
85
+
86
+ _getFilterSearchParamValue(crudState, defaultSearchColumn, apiMode = false) {
87
+ const filter = crudState.filter || "";
88
+ if (!apiMode) {
89
+ return filter;
90
+ }
91
+ return this.isValidFilterQuery(filter) ? filter : `${defaultSearchColumn}:like:%${filter}%`;
92
+ }
93
+
94
+ }
@@ -0,0 +1,23 @@
1
+ h1 {
2
+ color: var(--pico-primary)
3
+ }
4
+
5
+ h2 {
6
+ color: var(--pico-primary)
7
+ }
8
+
9
+ h3 {
10
+ color: var(--pico-primary)
11
+ }
12
+
13
+ h4 {
14
+ color: var(--pico-primary)
15
+ }
16
+
17
+ h5 {
18
+ color: var(--pico-primary)
19
+ }
20
+
21
+ h6 {
22
+ color: var(--pico-primary)
23
+ }
@@ -0,0 +1,44 @@
1
+ // Hamburger menu functionality
2
+ const hamburgerMenu = document.querySelector('.hamburger-menu');
3
+ const layoutContainer = document.querySelector('.layout-container');
4
+
5
+ hamburgerMenu.addEventListener('click', function() {
6
+ layoutContainer.classList.toggle('menu-active');
7
+ });
8
+
9
+ // Close menu when clicking outside
10
+ document.addEventListener('click', function(event) {
11
+ if (!layoutContainer.contains(event.target) && !hamburgerMenu.contains(event.target)) {
12
+ layoutContainer.classList.remove('menu-active');
13
+ }
14
+ });
15
+
16
+ // Theme switcher functionality
17
+ const themeSelect = document.getElementById('theme-select');
18
+
19
+ function setTheme(theme) {
20
+ document.documentElement.setAttribute('data-theme', theme);
21
+ localStorage.setItem('theme', theme);
22
+ }
23
+
24
+ function getSavedTheme() {
25
+ return localStorage.getItem('theme') || 'auto';
26
+ }
27
+
28
+ const savedTheme = getSavedTheme();
29
+ setTheme(savedTheme);
30
+ themeSelect.value = savedTheme;
31
+
32
+ themeSelect.addEventListener('change', (e) => {
33
+ setTheme(e.target.value);
34
+ });
35
+
36
+ function updateAutoTheme() {
37
+ if (getSavedTheme() === 'auto') {
38
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
39
+ document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
40
+ }
41
+ }
42
+
43
+ updateAutoTheme();
44
+ window.matchMedia('(prefers-color-scheme: dark)').addListener(updateAutoTheme);
@@ -0,0 +1,102 @@
1
+ body {
2
+ overflow-x: hidden;
3
+ margin: 0;
4
+ padding: 0;
5
+ min-height: 100vh;
6
+ }
7
+ .layout-container {
8
+ display: flex;
9
+ min-height: 100vh;
10
+ }
11
+ aside {
12
+ position: fixed;
13
+ top: 0;
14
+ left: 0;
15
+ bottom: 0;
16
+ width: 300px;
17
+ padding: 1rem;
18
+ border-right: 1px solid var(--muted-border-color);
19
+ height: 100vh;
20
+ background: var(--background-color);
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+ aside nav {
25
+ flex-grow: 1; /* Pushes theme-switcher to the bottom */
26
+ overflow-y: auto;
27
+ overflow-x: hidden;
28
+ }
29
+ aside ul {
30
+ padding: 0;
31
+ list-style: none;
32
+ }
33
+ aside li {
34
+ margin-bottom: 0.5rem;
35
+ }
36
+ aside li > ul {
37
+ padding-left: 1rem;
38
+ }
39
+ .content-wrapper {
40
+ flex: 1;
41
+ overflow-x: hidden;
42
+ }
43
+ .content {
44
+ padding: 1rem;
45
+ }
46
+ .hamburger-menu {
47
+ display: none;
48
+ background: var(--pico-primary);
49
+ border: none;
50
+ font-size: 1.5rem;
51
+ cursor: pointer;
52
+ position: fixed;
53
+ top: 1rem;
54
+ left: 1rem;
55
+ z-index: 1001;
56
+ color: var(--contrast);
57
+ }
58
+ .theme-switcher {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 0.5rem;
62
+ margin-top: auto;
63
+ }
64
+ .active-link {
65
+ font-weight: bold;
66
+ color: var(--primary);
67
+ }
68
+
69
+ @media (max-width: 768px) {
70
+ .layout-container {
71
+ position: relative;
72
+ left: 0;
73
+ transition: left 0.3s ease;
74
+ }
75
+ .hamburger-menu {
76
+ display: block;
77
+ }
78
+ aside {
79
+ left: -300px;
80
+ top: 0;
81
+ bottom: 0;
82
+ z-index: 1000;
83
+ transition: left 0.3s ease;
84
+ padding-top: 4rem;
85
+ }
86
+ .layout-container.menu-active {
87
+ margin-left: 300px;
88
+ /*left: 300px;*/
89
+ }
90
+ .layout-container.menu-active aside {
91
+ left: 0;
92
+ }
93
+ .content {
94
+ padding-top: 4rem;
95
+ }
96
+ }
97
+
98
+ @media (min-width: 769px) {
99
+ .content-wrapper {
100
+ margin-left: 300px;
101
+ }
102
+ }
@@ -4,31 +4,86 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="light dark">
7
- {% for css_path in partials.css_path_list -%}
7
+ <link rel="stylesheet" href="{{pico_css_path}}">
8
+ <link rel="stylesheet" href="/static/default/pico-style.css">
9
+ <link rel="stylesheet" href="/static/default/style.css">
10
+ {% for css_path in css_path_list -%}
8
11
  <link rel="stylesheet" href="{{css_path}}">
9
12
  {% endfor %}
10
- <link rel="icon" href="{{partials.favicon_path}}" sizes="32x32" type="image/png">
11
- <title>{{partials.title}}</title>
13
+ <link rel="icon" href="{{favicon_path}}" sizes="32x32" type="image/png">
14
+ <title>{{title}}</title>
12
15
  </head>
13
16
  <body>
14
- <header class="container">
15
- <hgroup>
16
- <h1>{{partials.title}}</h1>
17
- <p>{{partials.subtitle}}</p>
17
+ <button class="hamburger-menu" aria-label="Menu">☰</button>
18
+ <div class="layout-container">
19
+ <aside>
18
20
  <nav>
19
21
  <ul>
20
- <li><a href="/">🏠 Home</a></li>
21
- <li><a href="/docs">💻 API Documentation</a></li>
22
- </ul>
23
- <ul>
24
- <li>Hi</li>
22
+ {% for navigation in navigations %}
23
+ {% if navigation|attr("pages") %}
24
+ <li>
25
+ <details {% if navigation.active %}open{% endif %}>
26
+ <summary>{{navigation.caption}}</summary>
27
+ <ul>
28
+ {% for page in navigation.pages %}
29
+ <li><a href="{{page.url}}" class="{{ 'active-link' if page.active else '' }}">{{page.caption}}</a></li>
30
+ {% endfor %}
31
+ </ul>
32
+ </details>
33
+ </li>
34
+ {% else %}
35
+ <li><a href="{{navigation.url}}" class="{{ 'active-link' if navigation.active else '' }}">{{navigation.caption}}</a></li>
36
+ {% endif %}
37
+ {% endfor %}
25
38
  </ul>
26
39
  </nav>
27
- </hgroup>
28
- </header>
29
- {{content}}
40
+ <div class="theme-switcher">
41
+ <label for="theme-select">Theme:</label>
42
+ <select id="theme-select">
43
+ <option value="auto">🌗 Auto</option>
44
+ <option value="light">☀️ Light</option>
45
+ <option value="dark">🌙 Dark</option>
46
+ </select>
47
+ </div>
48
+ </aside>
49
+ <div class="content-wrapper">
50
+ <main class="content">
51
+ <header>
52
+ <hgroup>
53
+ <h1>{{title}}</h1>
54
+ <p>{{subtitle}}</p>
55
+ </hgroup>
56
+ {% if show_user_info %}
57
+ <nav>
58
+ <ul></ul>
59
+ <ul>
60
+ <li>
61
+ {% if current_user is none %}
62
+ <p>Hi Visitor <a href="/login">🔓</a></p>
63
+ {% elif current_user.is_guest %}
64
+ <p>Hi {{current_user.username}} <a href="/login">🔓</a></p>
65
+ {% else %}
66
+ <p>Hi {{current_user.username}} <a href="/logout">🔒</a></p>
67
+ {% endif %}
68
+ </li>
69
+ </ul>
70
+ </nav>
71
+ {% endif %}
72
+ </header>
73
+ <script src="/static/common/util.js"></script>
74
+ {{content}}
75
+ <footer>{{footer}}</footer>
76
+ </main>
77
+ </div>
78
+ </div>
79
+ {% for js_path in js_path_list -%}
80
+ <script src="{{js_path}}"></script>
81
+ {% endfor %}
82
+ <script src="/static/default/script.js"></script>
83
+ <script>
84
+ {% if should_refresh_session %}
85
+ UTIL.refreshAccessTokenPeriodically({{refresh_session_interval_seconds}});
86
+ {% endif %}
87
+ </script>
30
88
  </body>
31
- {% for js_path in partials.js_path_list -%}
32
- <script src="{{js_path}}"></script>
33
- {% endfor %}
34
89
  </html>
@@ -4,4 +4,9 @@ sqlmodel~=0.0.22
4
4
  ulid-py~=1.1.0
5
5
  passlib~=1.7.4
6
6
  Jinja2~=3.1.5
7
- python-jose~=3.3.0
7
+ python-jose~=3.4.0
8
+ passlib~=1.7.4
9
+
10
+ pytest~=8.3.4
11
+ pytest-asyncio~=0.24.0
12
+ pytest-cov~=6.0.0
@@ -34,6 +34,7 @@ class PermissionUpdateWithAudit(PermissionUpdate):
34
34
 
35
35
  class PermissionResponse(PermissionBase):
36
36
  id: str
37
+ description: str
37
38
 
38
39
 
39
40
  class MultiplePermissionResponse(BaseModel):
@@ -90,6 +90,12 @@ class AuthUserResponse(UserResponse):
90
90
  is_super_user: bool
91
91
  is_guest: bool
92
92
 
93
+ def has_permission(self, permission_name: str):
94
+ return self.is_super_user or permission_name in self.permission_names
95
+
96
+ def has_role(self, role_name: str):
97
+ return self.is_super_user or role_name in self.role_names
98
+
93
99
 
94
100
  class MultipleUserResponse(BaseModel):
95
101
  data: list[UserResponse]
@@ -111,6 +117,9 @@ class UserTokenData(SQLModel):
111
117
  class UserSessionResponse(SQLModel):
112
118
  id: str
113
119
  user_id: str
120
+ access_token: str
121
+ refresh_token: str
122
+ token_type: str
114
123
  access_token_expired_at: datetime.datetime
115
124
  refresh_token_expired_at: datetime.datetime
116
125
 
@@ -0,0 +1,19 @@
1
+ import time
2
+
3
+ from fastapi.testclient import TestClient
4
+ from my_app_name.config import APP_AUTH_SUPER_USER, APP_AUTH_SUPER_USER_PASSWORD
5
+ from my_app_name.main import app
6
+
7
+
8
+ def get_admin_access_token():
9
+ client = TestClient(app, base_url="http://localhost")
10
+ # Create new admin user session and check the response
11
+ session_response = client.post(
12
+ "/api/v1/user-sessions",
13
+ data={
14
+ "username": APP_AUTH_SUPER_USER,
15
+ "password": APP_AUTH_SUPER_USER_PASSWORD,
16
+ },
17
+ )
18
+ session_data = session_response.json()
19
+ return session_data.get("access_token")
@@ -0,0 +1,59 @@
1
+ from fastapi.testclient import TestClient
2
+ from my_app_name.main import app
3
+ from my_app_name.test._util.access_token import get_admin_access_token
4
+
5
+
6
+ def test_create_permission():
7
+ client = TestClient(app, base_url="http://localhost")
8
+ access_token = get_admin_access_token()
9
+ new_permission_data = {
10
+ "name": "new-permission",
11
+ "description": "new-permission-description",
12
+ }
13
+ response = client.post(
14
+ "/api/v1/permissions",
15
+ json=new_permission_data,
16
+ headers={"Authorization": f"Bearer {access_token}"},
17
+ )
18
+ assert response.status_code == 200
19
+ response_data = response.json()
20
+ assert response_data.get("id") is not None
21
+ assert response_data.get("id") != ""
22
+ assert response_data.get("name") == "new-permission"
23
+ assert response_data.get("description") == "new-permission-description"
24
+
25
+
26
+ def test_create_permission_bulk():
27
+ client = TestClient(app, base_url="http://localhost")
28
+ access_token = get_admin_access_token()
29
+ new_first_permission_data = {
30
+ "name": "new-permission-bulk-1",
31
+ "description": "new-permission-bulk-description-1",
32
+ }
33
+ new_second_permission_data = {
34
+ "name": "new-permission-bulk-2",
35
+ "description": "new-permission-bulk-description-2",
36
+ }
37
+ new_permission_data = [new_first_permission_data, new_second_permission_data]
38
+ response = client.post(
39
+ "/api/v1/permissions/bulk",
40
+ json=new_permission_data,
41
+ headers={"Authorization": f"Bearer {access_token}"},
42
+ )
43
+ assert response.status_code == 200
44
+ response_data = response.json()
45
+ assert len(response_data) == 2
46
+ # Id should not be empty
47
+ assert response_data[0] is not None
48
+ assert response_data[0] != ""
49
+ assert response_data[1] is not None
50
+ assert response_data[1] != ""
51
+ # Data should match
52
+ assert new_first_permission_data["name"] in [row["name"] for row in response_data]
53
+ assert new_second_permission_data["name"] in [row["name"] for row in response_data]
54
+ assert new_first_permission_data["description"] in [
55
+ row["description"] for row in response_data
56
+ ]
57
+ assert new_second_permission_data["description"] in [
58
+ row["description"] for row in response_data
59
+ ]
@@ -0,0 +1,68 @@
1
+ from fastapi.testclient import TestClient
2
+ from my_app_name.main import app
3
+ from my_app_name.test._util.access_token import get_admin_access_token
4
+
5
+
6
+ def test_delete_permission():
7
+ client = TestClient(app, base_url="http://localhost")
8
+ access_token = get_admin_access_token()
9
+ new_permission_data = {
10
+ "name": "to-be-deleted-permission",
11
+ "description": "to-be-deleted-permission-description",
12
+ }
13
+ insert_response = client.post(
14
+ "/api/v1/permissions",
15
+ json=new_permission_data,
16
+ headers={"Authorization": f"Bearer {access_token}"},
17
+ )
18
+ assert insert_response.status_code == 200
19
+ id = insert_response.json().get("id")
20
+ # deleting
21
+ response = client.delete(
22
+ f"/api/v1/permissions/{id}", headers={"Authorization": f"Bearer {access_token}"}
23
+ )
24
+ assert response.status_code == 200
25
+ response_data = response.json()
26
+ assert response_data.get("id") == id
27
+ assert response_data.get("name") == "to-be-deleted-permission"
28
+ assert response_data.get("description") == "to-be-deleted-permission-description"
29
+
30
+
31
+ def test_delete_permission_bulk():
32
+ client = TestClient(app, base_url="http://localhost")
33
+ access_token = get_admin_access_token()
34
+ new_first_permission_data = {
35
+ "name": "to-be-deleted-permission-bulk-1",
36
+ "description": "to-be-deleted-permission-bulk-description-1",
37
+ }
38
+ new_second_permission_data = {
39
+ "name": "to-be-deleted-permission-bulk-2",
40
+ "description": "to-be-deleted-permission-bulk-description-2",
41
+ }
42
+ new_permission_data = [new_first_permission_data, new_second_permission_data]
43
+ insert_response = client.post(
44
+ "/api/v1/permissions/bulk",
45
+ json=new_permission_data,
46
+ headers={"Authorization": f"Bearer {access_token}"},
47
+ )
48
+ assert insert_response.status_code == 200
49
+ ids = [row["id"] for row in insert_response.json()]
50
+ # deleting (use client.request since client.delete doesn't support json param)
51
+ response = client.request(
52
+ "DELETE",
53
+ f"/api/v1/permissions/bulk",
54
+ json=ids,
55
+ headers={"Authorization": f"Bearer {access_token}"},
56
+ )
57
+ assert response.status_code == 200
58
+ response_data = response.json()
59
+ # Data should match
60
+ assert len([row["id"] for row in response_data if row["id"] in ids]) == 2
61
+ assert new_first_permission_data["name"] in [row["name"] for row in response_data]
62
+ assert new_second_permission_data["name"] in [row["name"] for row in response_data]
63
+ assert new_first_permission_data["description"] in [
64
+ row["description"] for row in response_data
65
+ ]
66
+ assert new_second_permission_data["description"] in [
67
+ row["description"] for row in response_data
68
+ ]
@@ -0,0 +1,71 @@
1
+ from fastapi.testclient import TestClient
2
+ from my_app_name.main import app
3
+ from my_app_name.test._util.access_token import get_admin_access_token
4
+
5
+
6
+ def test_read_permission_by_id():
7
+ client = TestClient(app, base_url="http://localhost")
8
+ access_token = get_admin_access_token()
9
+ new_permission_data = {
10
+ "name": "to-be-read-permission",
11
+ "description": "to-be-read-permission-description",
12
+ }
13
+ insert_response = client.post(
14
+ "/api/v1/permissions",
15
+ json=new_permission_data,
16
+ headers={"Authorization": f"Bearer {access_token}"},
17
+ )
18
+ assert insert_response.status_code == 200
19
+ id = insert_response.json().get("id")
20
+ # fetching
21
+ response = client.get(
22
+ f"/api/v1/permissions/{id}", headers={"Authorization": f"Bearer {access_token}"}
23
+ )
24
+ assert response.status_code == 200
25
+ response_data = response.json()
26
+ assert response_data.get("id") == id
27
+ assert response_data.get("name") == "to-be-read-permission"
28
+ assert response_data.get("description") == "to-be-read-permission-description"
29
+
30
+
31
+ def test_read_permission_bulk():
32
+ client = TestClient(app, base_url="http://localhost")
33
+ access_token = get_admin_access_token()
34
+ new_first_permission_data = {
35
+ "name": "to-be-read-permission-bulk-1",
36
+ "description": "to-be-read-permission-bulk-description-1",
37
+ }
38
+ new_second_permission_data = {
39
+ "name": "to-be-read-permission-bulk-2",
40
+ "description": "to-be-read-permission-bulk-description-2",
41
+ }
42
+ new_permission_data = [new_first_permission_data, new_second_permission_data]
43
+ insert_response = client.post(
44
+ "/api/v1/permissions/bulk",
45
+ json=new_permission_data,
46
+ headers={"Authorization": f"Bearer {access_token}"},
47
+ )
48
+ assert insert_response.status_code == 200
49
+ ids = [row["id"] for row in insert_response.json()]
50
+ # fetching
51
+ response = client.get(
52
+ f"/api/v1/permissions",
53
+ params={
54
+ "filter": "name:like:to-be-read-permission-bulk-%",
55
+ },
56
+ headers={"Authorization": f"Bearer {access_token}"},
57
+ )
58
+ assert response.status_code == 200
59
+ response_data_count = response.json()["count"]
60
+ assert response_data_count == 2
61
+ response_data = response.json()["data"]
62
+ # Data should match
63
+ assert len([row["id"] for row in response_data if row["id"] in ids]) == 2
64
+ assert new_first_permission_data["name"] in [row["name"] for row in response_data]
65
+ assert new_second_permission_data["name"] in [row["name"] for row in response_data]
66
+ assert new_first_permission_data["description"] in [
67
+ row["description"] for row in response_data
68
+ ]
69
+ assert new_second_permission_data["description"] in [
70
+ row["description"] for row in response_data
71
+ ]