stkit 17.0.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 (128) hide show
  1. stk/__init__.py +19 -0
  2. stk/__main__.py +6 -0
  3. stk/agent_login.py +78 -0
  4. stk/app.py +323 -0
  5. stk/checks.py +634 -0
  6. stk/cli/__init__.py +49 -0
  7. stk/cli/agent.py +267 -0
  8. stk/cli/base.py +79 -0
  9. stk/cli/database.py +199 -0
  10. stk/cli/develop.py +63 -0
  11. stk/cli/doctor.py +262 -0
  12. stk/cli/main.py +101 -0
  13. stk/cli/reports.py +296 -0
  14. stk/cli/smoke.py +399 -0
  15. stk/cli/users.py +259 -0
  16. stk/cli/watch.py +68 -0
  17. stk/extensions.py +29 -0
  18. stk/migrations/__init__.py +130 -0
  19. stk/migrations/env.py +58 -0
  20. stk/migrations/script.py.mako +17 -0
  21. stk/migrations/versions/stk_0001_initial_schema.py +213 -0
  22. stk/portal/views.py +34 -0
  23. stk/public/__init__.py +0 -0
  24. stk/public/models.py +0 -0
  25. stk/public/views.py +344 -0
  26. stk/scaffold/__init__.py +1 -0
  27. stk/scaffold/app/CLAUDE.md +65 -0
  28. stk/scaffold/app/Dockerfile +41 -0
  29. stk/scaffold/app/README.md +16 -0
  30. stk/scaffold/app/__app__/__init__.py +1 -0
  31. stk/scaffold/app/__app__/app.py +8 -0
  32. stk/scaffold/app/__app__/settings.py +7 -0
  33. stk/scaffold/app/claude/skills/stk-api/SKILL.md +52 -0
  34. stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +48 -0
  35. stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +61 -0
  36. stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +58 -0
  37. stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +96 -0
  38. stk/scaffold/app/docker-compose.yml +88 -0
  39. stk/scaffold/app/dockerignore +13 -0
  40. stk/scaffold/app/env-sample +78 -0
  41. stk/scaffold/app/gitignore +8 -0
  42. stk/scaffold/app/nginx/nginx.conf +46 -0
  43. stk/scaffold/app/nginx/stk.conf +36 -0
  44. stk/scaffold/app/postgres/init-db.sh +8 -0
  45. stk/scaffold/app/pyproject.toml +23 -0
  46. stk/scaffold/app/tests/__init__.py +0 -0
  47. stk/scaffold/app/tests/test_app.py +23 -0
  48. stk/scaffold/generator.py +209 -0
  49. stk/scaffold/templates.py +437 -0
  50. stk/security.py +145 -0
  51. stk/settings.py +147 -0
  52. stk/shell.py +284 -0
  53. stk/static/VERSIONS.txt +4 -0
  54. stk/static/css/app.css +229 -0
  55. stk/static/css/layout.css +330 -0
  56. stk/static/css/vuetify.min.css +6 -0
  57. stk/static/icons/fonts/tabler-icons.woff2 +0 -0
  58. stk/static/icons/tabler-icons.min.css +4 -0
  59. stk/static/img/auth-bg.webp +0 -0
  60. stk/static/img/favicon.ico +0 -0
  61. stk/static/img/stk.svg +1664 -0
  62. stk/static/js/axios.min.js +5 -0
  63. stk/static/js/components/NotificationDropdown.js +156 -0
  64. stk/static/js/components/ThemeSwitcher.js +68 -0
  65. stk/static/js/components/TransitionExpand.js +64 -0
  66. stk/static/js/components/VerticalNavGroup.js +119 -0
  67. stk/static/js/components/VerticalNavLink.js +36 -0
  68. stk/static/js/components/VerticalNavSectionTitle.js +21 -0
  69. stk/static/js/components/index.js +47 -0
  70. stk/static/js/config.js +150 -0
  71. stk/static/js/vue.min.js +13 -0
  72. stk/static/js/vuetify.min.js +3148 -0
  73. stk/static/robots.txt +1 -0
  74. stk/tasks/__init__.py +78 -0
  75. stk/templates/401.html +14 -0
  76. stk/templates/404.html +12 -0
  77. stk/templates/500.html +13 -0
  78. stk/templates/_brand.html +1 -0
  79. stk/templates/auth_layout.html +26 -0
  80. stk/templates/cms/activities.html +135 -0
  81. stk/templates/cms/roles.html +215 -0
  82. stk/templates/cms/users.html +312 -0
  83. stk/templates/dashboard.html +132 -0
  84. stk/templates/index.html +52 -0
  85. stk/templates/layout.html +328 -0
  86. stk/templates/login-layout.html +54 -0
  87. stk/templates/security/_macros.html +39 -0
  88. stk/templates/security/_messages.html +12 -0
  89. stk/templates/security/change_password.html +100 -0
  90. stk/templates/security/confirm_email.html +44 -0
  91. stk/templates/security/email/change_notice.html +4 -0
  92. stk/templates/security/email/change_notice.txt +5 -0
  93. stk/templates/security/email/confirmation_instructions.html +3 -0
  94. stk/templates/security/email/confirmation_instructions.txt +3 -0
  95. stk/templates/security/email/login_instructions.html +5 -0
  96. stk/templates/security/email/login_instructions.txt +5 -0
  97. stk/templates/security/email/reset_instructions.html +1 -0
  98. stk/templates/security/email/reset_instructions.txt +3 -0
  99. stk/templates/security/email/reset_notice.html +1 -0
  100. stk/templates/security/email/reset_notice.txt +1 -0
  101. stk/templates/security/email/welcome.html +7 -0
  102. stk/templates/security/email/welcome.txt +7 -0
  103. stk/templates/security/forgot_password.html +63 -0
  104. stk/templates/security/login_user.html +206 -0
  105. stk/templates/security/mf_recovery.html +67 -0
  106. stk/templates/security/mf_recovery_codes.html +119 -0
  107. stk/templates/security/register_user.html +263 -0
  108. stk/templates/security/reset_password.html +53 -0
  109. stk/templates/security/send_confirmation.html +48 -0
  110. stk/templates/security/send_login.html +39 -0
  111. stk/templates/security/two_factor_select.html +78 -0
  112. stk/templates/security/two_factor_setup.html +166 -0
  113. stk/templates/security/two_factor_verify_code.html +71 -0
  114. stk/templates/security/wan_register.html +179 -0
  115. stk/templates/security/wan_signin.html +123 -0
  116. stk/templates/security/wan_verify.html +109 -0
  117. stk/user/__init__.py +0 -0
  118. stk/user/forms.py +10 -0
  119. stk/user/models.py +308 -0
  120. stk/user/views.py +339 -0
  121. stk/utils/email.py +32 -0
  122. stk/utils/ratelimit.py +67 -0
  123. stk/websocket.py +133 -0
  124. stkit-17.0.0.dist-info/METADATA +135 -0
  125. stkit-17.0.0.dist-info/RECORD +128 -0
  126. stkit-17.0.0.dist-info/WHEEL +4 -0
  127. stkit-17.0.0.dist-info/entry_points.txt +2 -0
  128. stkit-17.0.0.dist-info/licenses/LICENSE +21 -0
stk/static/robots.txt ADDED
@@ -0,0 +1 @@
1
+ User-agent: *
stk/tasks/__init__.py ADDED
@@ -0,0 +1,78 @@
1
+ """Async background tasks.
2
+
3
+ Usage:
4
+ from stk.tasks import run_in_background
5
+
6
+ async def my_handler():
7
+ await run_in_background(send_welcome_email(user_id=123))
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def run_in_background(coro):
17
+ """Fire-and-forget async task with error logging.
18
+
19
+ Use for work that doesn't need a result: emails, webhooks, cache warming, etc.
20
+ The task runs in the current event loop, no extra processes, no Redis.
21
+ """
22
+ task = asyncio.create_task(coro)
23
+ task.add_done_callback(_log_task_exception)
24
+ return task
25
+
26
+
27
+ async def run_with_session(coro_factory):
28
+ """Fire-and-forget async task that gets its own DB session.
29
+
30
+ coro_factory is an async callable that takes a session argument:
31
+ async def my_task(session):
32
+ await session.execute(...)
33
+
34
+ await run_with_session(my_task)
35
+ """
36
+ import stk.extensions as ext
37
+
38
+ async def _wrapper():
39
+ async with ext.async_session_factory() as session:
40
+ try:
41
+ await coro_factory(session)
42
+ await session.commit()
43
+ except Exception:
44
+ await session.rollback()
45
+ raise
46
+
47
+ return await run_in_background(_wrapper())
48
+
49
+
50
+ def _log_task_exception(task: asyncio.Task):
51
+ if not task.cancelled() and task.exception():
52
+ logger.error(
53
+ "Background task failed: %s", task.exception(), exc_info=task.exception()
54
+ )
55
+
56
+
57
+ async def cleanup_expired_sessions():
58
+ """Deactivate expired sessions and delete very old rows."""
59
+ from datetime import datetime, timedelta
60
+
61
+ from sqlalchemy import delete, update
62
+
63
+ import stk.extensions as ext
64
+ from stk.user.models import Session
65
+
66
+ async with ext.async_session_factory() as session:
67
+ lifetime = 3600
68
+ cutoff = datetime.now() - timedelta(seconds=lifetime)
69
+ delete_cutoff = datetime.now() - timedelta(days=30)
70
+
71
+ await session.execute(
72
+ update(Session)
73
+ .where(Session.is_active == True, Session.created_at < cutoff) # noqa: E712
74
+ .values(is_active=False)
75
+ )
76
+ await session.execute(delete(Session).where(Session.created_at < delete_cutoff))
77
+ await session.commit()
78
+ logger.info("Session cleanup complete")
stk/templates/401.html ADDED
@@ -0,0 +1,14 @@
1
+
2
+ {% extends "layout.html" %}
3
+
4
+ {% block page_title %}Unauthorized{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="jumbotron">
8
+ <div class="text-center">
9
+ <h1>401</h1>
10
+ <p>You are not authorized to see this page.
11
+ </p>
12
+ </div>
13
+ </div>
14
+ {% endblock %}
stk/templates/404.html ADDED
@@ -0,0 +1,12 @@
1
+
2
+ {% block page_title %}{% endblock %}
3
+
4
+ {% block content %}
5
+
6
+ <div style="padding: 50px;margin: 100px auto; text-align: center;">
7
+ <h1 style="font-size: 90px;font-weight: bold;">404</h1>
8
+ <p>Sorry, that page doesn't exist.</p>
9
+
10
+ </div>
11
+
12
+ {% endblock %}
stk/templates/500.html ADDED
@@ -0,0 +1,13 @@
1
+
2
+ {% extends "layout.html" %}
3
+
4
+ {% block page_title %}Server error{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="jumbotron">
8
+ <div class="text-center">
9
+ <h1>500</h1>
10
+ <p>Sorry, something went wrong on our system. Don't panic, we are fixing it! Please try again later.</p>
11
+ </div>
12
+ </div>
13
+ {% endblock %}
@@ -0,0 +1 @@
1
+ <img src="/static/img/stk.svg" alt="{{ config.APP_NAME }}" height="{{ brand_size|default(40) }}" class="logo-adaptive">
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>{{ config.APP_NAME }}</title>
6
+ <meta name="description" content="A framework for the next decade">
7
+ <meta name="author" content="{{ config.APP_NAME }}">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1">
9
+ <link rel="icon" href="/static/img/favicon.ico" type="image/x-icon">
10
+
11
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/vuetify.min.css') }}">
12
+ <link rel="stylesheet" href="{{ url_for('static', filename='icons/tabler-icons.min.css') }}">
13
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
14
+
15
+ {% block css %}{% endblock %}
16
+ </head>
17
+ <body>
18
+ {% block content %}{% endblock %}
19
+
20
+ <script src="{{ url_for('static', filename='js/vue.min.js') }}"></script>
21
+ <script src="{{ url_for('static', filename='js/vuetify.min.js') }}"></script>
22
+ <script src="{{ url_for('static', filename='js/axios.min.js') }}"></script>
23
+ <script src="{{ url_for('static', filename='js/config.js') }}"></script>
24
+ {% block js %}{% endblock %}
25
+ </body>
26
+ </html>
@@ -0,0 +1,135 @@
1
+ {% extends 'layout.html' %}
2
+ {% block css %}
3
+ {% endblock %}
4
+ {% block content %}
5
+
6
+ <v-card class="ma-2 mt-12 w-100 h-100">
7
+ <v-toolbar>
8
+ <v-toolbar-title>Activity Logs</v-toolbar-title>
9
+ <v-spacer></v-spacer>
10
+ </v-toolbar>
11
+ <v-card-text>
12
+
13
+ <v-data-table-server
14
+ :headers="headers"
15
+ :items="items"
16
+ :items-length="itemsLength"
17
+ :page="options.page"
18
+ :items-per-page="options.itemsPerPage"
19
+ @update:options="refresh"
20
+ hover
21
+ >
22
+ <template v-slot:item.data="{ item }">
23
+ <v-btn size="small" variant="text" color="primary" @click="showData(item)">
24
+ View Data
25
+ </v-btn>
26
+ </template>
27
+ </v-data-table-server>
28
+
29
+ </v-card-text>
30
+ </v-card>
31
+
32
+ <!-- Data Dialog -->
33
+ <v-dialog v-model="dataDialog" width="800">
34
+ <v-card v-if="dataDialog" class="pa-4">
35
+ <v-card-title class="d-flex align-center">
36
+ Activity Data
37
+ <v-spacer></v-spacer>
38
+ <v-btn icon="ti ti-x" size="small" variant="text" @click="dataDialog = false"></v-btn>
39
+ </v-card-title>
40
+
41
+ <v-card-text>
42
+ <pre class="pa-4 rounded bg-grey-lighten-4"><code>${JSON.stringify(currentData, null, 2)}</code></pre>
43
+ </v-card-text>
44
+ </v-card>
45
+ </v-dialog>
46
+
47
+ <v-snackbar size="small" class="d-flex" v-model="snackBar" rounded="pill" elevation="25">
48
+ ${snackMessage}
49
+ <template v-slot:actions>
50
+ <v-btn @click="snackBar=false" icon="ti ti-x" class="ml-auto" variant="text" size="small"></v-btn>
51
+ </template>
52
+ </v-snackbar>
53
+
54
+ {% endblock %} {% block js %}
55
+
56
+ <script>
57
+ const {createApp} = Vue;
58
+ const {createVuetify} = Vuetify;
59
+
60
+ const vuetify = createVuetify(config.vuetifyConfig);
61
+
62
+ window.app = createApp({
63
+ mixins: [layoutMixin],
64
+ data() {
65
+ return {
66
+ snackBar: false,
67
+ snackMessage: "",
68
+ items: [],
69
+ itemsLength: 0,
70
+ options: {
71
+ page: 1,
72
+ itemsPerPage: 25
73
+ },
74
+ dataDialog: false,
75
+ currentData: null,
76
+
77
+ headers: [
78
+ {title: 'ID', value: 'id', sortable: true},
79
+ {title: 'User', value: 'user', sortable: true},
80
+ {title: 'Action', value: 'action', sortable: true},
81
+ {title: 'Date', value: 'created_at', sortable: true},
82
+ {title: 'Data', value: 'data', sortable: false}
83
+ ]
84
+ };
85
+ },
86
+
87
+ mounted() {
88
+ // No refresh on mount
89
+ },
90
+ delimiters: config.delimiters,
91
+
92
+ methods: {
93
+ showSnack(message) {
94
+ this.snackMessage = message;
95
+ this.snackBar = true;
96
+ },
97
+
98
+ refresh(options) {
99
+ // Use options from parameter or component state
100
+ if (options) {
101
+ // Update options with the values from the event
102
+ this.options = {
103
+ ...this.options,
104
+ page: options.page,
105
+ itemsPerPage: options.itemsPerPage
106
+ };
107
+ }
108
+
109
+ // Make API request with pagination parameters
110
+ axios.get(`/api/activities?page=${this.options.page}&per_page=${this.options.itemsPerPage}`)
111
+ .then(res => {
112
+ this.items = res.data.items;
113
+ this.itemsLength = res.data.total;
114
+ // Update itemsPerPage from API response if provided
115
+ if (res.data.perPage) {
116
+ this.options.itemsPerPage = res.data.perPage;
117
+ }
118
+ })
119
+ .catch(error => {
120
+ console.error('Error fetching activities:', error);
121
+ this.showSnack('Failed to load activities');
122
+ });
123
+ },
124
+
125
+ showData(item) {
126
+ this.currentData = item.data;
127
+ this.dataDialog = true;
128
+ }
129
+ }
130
+ });
131
+
132
+ registerStkComponents(app);
133
+ app.use(vuetify).mount("#app");
134
+ </script>
135
+ {% endblock %}
@@ -0,0 +1,215 @@
1
+ {% extends 'layout.html' %}
2
+ {% block css %}
3
+ {% endblock %}
4
+ {% block content %}
5
+
6
+ <v-card class="ma-2 mt-12 w-100 h-100">
7
+ <v-toolbar>
8
+ <v-toolbar-title>Roles Dashboard</v-toolbar-title>
9
+ <v-spacer></v-spacer>
10
+ </v-toolbar>
11
+ <v-card-text>
12
+
13
+ <v-data-table-server
14
+ :headers="headers"
15
+ :items="items"
16
+ :items-length="itemsLength"
17
+ :page="options.page"
18
+ :items-per-page="options.itemsPerPage"
19
+ @update:options="refresh"
20
+ hover
21
+ >
22
+ <template v-slot:top>
23
+ <v-toolbar color="transparent" class="mb-4" dense elevation="0">
24
+ <v-btn class="ml-auto" @click="createItem" size="small" variant="elevated" color="primary">
25
+ <template v-slot:prepend><i class="ti ti-plus"></i></template>
26
+ Add Role
27
+ </v-btn>
28
+ </v-toolbar>
29
+ </template>
30
+
31
+ <template v-slot:item.description="{ item }">
32
+ <span>${item.description}</span>
33
+ </template>
34
+
35
+ <template v-slot:item.actions="{ item }">
36
+ <v-icon
37
+ small
38
+ class="mr-2"
39
+ @click="editItem(item)"
40
+ >
41
+ ti ti-pencil
42
+ </v-icon>
43
+ <v-icon
44
+ small
45
+ @click="deleteItem(item)"
46
+ >
47
+ ti ti-trash
48
+ </v-icon>
49
+ </template>
50
+
51
+ </v-data-table-server>
52
+
53
+
54
+ </v-card-text>
55
+ </v-card>
56
+
57
+ <!--Edit Dialog-->
58
+ <v-dialog v-model="edialog" width="660">
59
+ <v-card v-if="edialog" class="pa-6">
60
+ <v-sheet class="d-flex justify-end">
61
+ <v-btn @click="edialog=false" size="small" icon="ti ti-x" variant="text"></v-btn>
62
+ </v-sheet>
63
+ <v-card-title>Role Editor</v-card-title>
64
+
65
+ <v-card-text>
66
+ <!-- fields go here -->
67
+ <v-text-field label="Name" v-model="eitem.name"></v-text-field>
68
+ <v-textarea label="Description" v-model="eitem.description" rows="3"></v-textarea>
69
+ </v-card-text>
70
+
71
+ <v-card-actions>
72
+ <v-spacer></v-spacer>
73
+ <v-btn color="primary" variant="elevated" @click="saveItem">Save</v-btn>
74
+ </v-card-actions>
75
+ </v-card>
76
+
77
+ </v-dialog>
78
+
79
+ <v-snackbar size="small" class="d-flex" v-model="snackBar" rounded="pill" elevation="25">
80
+
81
+ ${snackMessage}
82
+ <template v-slot:actions>
83
+ <v-btn @click="snackBar=false" icon="ti ti-x" class="ml-auto" variant="text" size="small"></v-btn>
84
+ </template>
85
+ </v-snackbar>
86
+
87
+ {% endblock %} {% block js %}
88
+
89
+ <script>
90
+ const {createApp, toRaw} = Vue;
91
+ const {createVuetify} = Vuetify;
92
+
93
+ const vuetify = createVuetify(config.vuetifyConfig);
94
+
95
+ window.app = createApp({
96
+ mixins: [layoutMixin],
97
+ data() {
98
+ return {
99
+ snackBar: false,
100
+ snackMessage: "",
101
+ items: [],
102
+ itemsLength: 0,
103
+ options: {
104
+ page: 1,
105
+ itemsPerPage: 25
106
+ },
107
+
108
+ headers: [
109
+ {title: 'ID', value: 'id', sortable: true},
110
+ {title: 'Name', value: 'name', sortable: true},
111
+ {title: 'Description', value: 'description', sortable: false},
112
+ {title: 'Actions', value: 'actions', sortable: false}
113
+ ],
114
+ edialog: false,
115
+ eitem: {
116
+ id: "",
117
+ name: "",
118
+ description: ""
119
+ },
120
+ };
121
+ },
122
+
123
+ mounted() {
124
+ // No refresh on mount
125
+ },
126
+ delimiters: config.delimiters,
127
+
128
+ methods: {
129
+ showSnack(message) {
130
+ this.snackMessage = message;
131
+ this.snackBar = true;
132
+ },
133
+
134
+ refresh(options) {
135
+ // Use options from parameter or component state
136
+ if (options) {
137
+ // Update options with the values from the event
138
+ this.options = {
139
+ ...this.options,
140
+ page: options.page,
141
+ itemsPerPage: options.itemsPerPage
142
+ };
143
+ }
144
+
145
+ // Make API request with pagination parameters
146
+ axios.get(`/api/roles?page=${this.options.page}&per_page=${this.options.itemsPerPage}`)
147
+ .then(res => {
148
+ this.items = res.data.items;
149
+ this.itemsLength = res.data.total;
150
+ // Update itemsPerPage from API response if provided
151
+ if (res.data.perPage) {
152
+ this.options.itemsPerPage = res.data.perPage;
153
+ }
154
+ })
155
+ .catch(error => {
156
+ console.error('Error fetching roles:', error);
157
+ });
158
+ },
159
+
160
+ createItem() {
161
+ this.edialog = true;
162
+ this.eitem = {};
163
+ },
164
+
165
+ editItem(item) {
166
+ this.eitem = toRaw(item);
167
+ this.$nextTick(() => {
168
+ this.edialog = true;
169
+ });
170
+ },
171
+
172
+ saveItem() {
173
+ if (this.eitem.id) {
174
+ // edit mode
175
+ axios.post(`/api/role/${this.eitem.id}`, {item: this.eitem})
176
+ .then(res => {
177
+ this.showSnack(res.data?.message);
178
+ this.refresh();
179
+ })
180
+ .catch(err => {
181
+ this.showSnack(err.response?.data);
182
+ });
183
+ } else {
184
+ // create mode
185
+ axios.post('/api/role/', {item: this.eitem})
186
+ .then(res => {
187
+ this.showSnack(res.data?.message);
188
+ this.refresh();
189
+ })
190
+ .catch(err => {
191
+ this.showSnack(err.response?.data);
192
+ });
193
+ }
194
+ this.edialog = false;
195
+ },
196
+
197
+ deleteItem(item) {
198
+ if (confirm('Are you sure?')) {
199
+ axios.delete(`/api/role/${item.id}`)
200
+ .then(res => {
201
+ this.showSnack(res.data?.message);
202
+ this.refresh();
203
+ })
204
+ .catch(err => {
205
+ this.showSnack(err.response?.data);
206
+ });
207
+ }
208
+ }
209
+ }
210
+ });
211
+
212
+ registerStkComponents(app);
213
+ app.use(vuetify).mount("#app");
214
+ </script>
215
+ {% endblock %}