django-spire 0.16.13__py3-none-any.whl → 0.17.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 (36) hide show
  1. django_spire/consts.py +1 -1
  2. django_spire/core/management/commands/spire_startapp.py +84 -46
  3. django_spire/core/management/commands/spire_startapp_pkg/__init__.py +60 -0
  4. django_spire/core/management/commands/spire_startapp_pkg/builder.py +91 -0
  5. django_spire/core/management/commands/spire_startapp_pkg/config.py +115 -0
  6. django_spire/core/management/commands/spire_startapp_pkg/filesystem.py +125 -0
  7. django_spire/core/management/commands/spire_startapp_pkg/generator.py +167 -0
  8. django_spire/core/management/commands/spire_startapp_pkg/maps.py +783 -25
  9. django_spire/core/management/commands/spire_startapp_pkg/permissions.py +147 -0
  10. django_spire/core/management/commands/spire_startapp_pkg/processor.py +144 -57
  11. django_spire/core/management/commands/spire_startapp_pkg/registry.py +89 -0
  12. django_spire/core/management/commands/spire_startapp_pkg/reporter.py +245 -108
  13. django_spire/core/management/commands/spire_startapp_pkg/resolver.py +86 -0
  14. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +252 -0
  15. django_spire/core/management/commands/spire_startapp_pkg/validator.py +96 -0
  16. django_spire/core/middleware/__init__.py +1 -2
  17. django_spire/profiling/__init__.py +13 -0
  18. django_spire/profiling/middleware/__init__.py +6 -0
  19. django_spire/{core → profiling}/middleware/profiling.py +63 -58
  20. django_spire/profiling/panel.py +345 -0
  21. django_spire/profiling/templates/panel.html +166 -0
  22. {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/METADATA +1 -1
  23. {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/RECORD +26 -23
  24. django_spire/core/management/commands/spire_startapp_pkg/constants.py +0 -4
  25. django_spire/core/management/commands/spire_startapp_pkg/manager.py +0 -176
  26. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_detail_card.html +0 -24
  27. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html +0 -9
  28. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html +0 -18
  29. django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html +0 -22
  30. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html +0 -24
  31. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html +0 -13
  32. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html +0 -13
  33. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html +0 -9
  34. {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/WHEEL +0 -0
  35. {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/licenses/LICENSE.md +0 -0
  36. {django_spire-0.16.13.dist-info → django_spire-0.17.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,345 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+
6
+ from dataclasses import asdict, dataclass
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing_extensions import TYPE_CHECKING
10
+
11
+ from debug_toolbar.panels import Panel
12
+ from django.conf import settings
13
+ from django.http import Http404, HttpRequest, HttpResponse, JsonResponse
14
+ from django.urls import re_path
15
+ from django.utils import timezone
16
+ from django.views.decorators.csrf import csrf_exempt
17
+
18
+ from django_spire.profiling import lock
19
+
20
+ if TYPE_CHECKING:
21
+ from typing_extensions import Any
22
+
23
+
24
+ @dataclass
25
+ class Profile:
26
+ filename: str
27
+ path: str
28
+ method: str
29
+ duration: str
30
+ profile_id: str
31
+ size: str
32
+ modified: str
33
+ timestamp: float
34
+
35
+
36
+ @dataclass
37
+ class ProfileStats:
38
+ profiles: list[Profile]
39
+ count: int
40
+ directory: str
41
+ total_size: str
42
+ enabled: bool
43
+
44
+
45
+ @dataclass
46
+ class ProfileList:
47
+ profiles: list[dict[str, Any]]
48
+ count: int
49
+ total_size: str
50
+
51
+
52
+ class ProfilingPanel(Panel):
53
+ nav_title = 'Profiles'
54
+ template = 'panel.html'
55
+ title = 'Profiling'
56
+
57
+ def __init__(self, *args, **kwargs):
58
+ super().__init__(*args, **kwargs)
59
+
60
+ self.directory = self._get_directory()
61
+
62
+ def _format_size(self, size: int) -> str:
63
+ for unit in ['B', 'KB', 'MB', 'GB']:
64
+ if size < 1024.0:
65
+ return f'{size:.1f} {unit}'
66
+
67
+ size /= 1024.0
68
+
69
+ return f'{size:.1f} TB'
70
+
71
+ def _get_directory(self) -> Path:
72
+ location = os.getenv('PROFILING_DIR', '.profile')
73
+
74
+ if isinstance(location, str):
75
+ if not Path(location).is_absolute():
76
+ current = Path.cwd()
77
+ base = getattr(settings, 'BASE_DIR', current)
78
+ location = Path(base) / location
79
+ else:
80
+ location = Path(location)
81
+
82
+ return Path(location)
83
+
84
+ def _get_files(self) -> list[Profile]:
85
+ profiles = []
86
+
87
+ if not self.directory.exists():
88
+ return profiles
89
+
90
+ with lock:
91
+ files = self._list_files()
92
+
93
+ for file in files:
94
+ if not file.exists():
95
+ continue
96
+
97
+ profile = self._parse_file(file)
98
+
99
+ if profile:
100
+ profiles.append(profile)
101
+
102
+ return profiles
103
+
104
+ def _get_size(self) -> int:
105
+ total = 0
106
+
107
+ if self.directory.exists():
108
+ with lock:
109
+ for file in self.directory.glob('*.html'):
110
+ if file.exists():
111
+ total = total + file.stat().st_size
112
+
113
+ return total
114
+
115
+ def _list_files(self) -> list[Path]:
116
+ files = [
117
+ file for file in self.directory.glob('*.html')
118
+ if file.exists()
119
+ ]
120
+
121
+ files.sort(
122
+ key=lambda p: p.stat().st_mtime if p.exists() else 0,
123
+ reverse=True
124
+ )
125
+
126
+ return files
127
+
128
+ def _parse_file(self, file: Path) -> Profile | None:
129
+ stat = file.stat()
130
+ parts = file.stem.split('_')
131
+
132
+ timestamp, method, path_part, duration, profile_id = self._parse_filename(
133
+ parts,
134
+ stat
135
+ )
136
+
137
+ display = self._parse_path(path_part)
138
+
139
+ mst = timezone.get_fixed_timezone(-360)
140
+ modified = datetime.fromtimestamp(timestamp, tz=mst)
141
+
142
+ return Profile(
143
+ filename=file.name,
144
+ path=display,
145
+ method=method,
146
+ duration=duration,
147
+ profile_id=profile_id,
148
+ size=self._format_size(stat.st_size),
149
+ modified=modified.strftime('%b %d, %Y %I:%M %p MST'),
150
+ timestamp=timestamp
151
+ )
152
+
153
+ def _parse_filename(self, parts: list[str], stat: Any) -> tuple[float, str, str, str, str]:
154
+ if len(parts) >= 4:
155
+ timestamp = int(parts[0]) / 1000
156
+ method = parts[1]
157
+ path_part = '_'.join(parts[2:-2])
158
+ duration = parts[-2] if len(parts) > 3 else 'N/A'
159
+ profile_id = parts[-1] if len(parts) > 4 else 'N/A'
160
+ else:
161
+ timestamp = stat.st_mtime
162
+ method = 'Unknown'
163
+ path_part = '_'.join(parts)
164
+ duration = 'N/A'
165
+ profile_id = 'N/A'
166
+
167
+ return timestamp, method, path_part, duration, profile_id
168
+
169
+ def _parse_path(self, path_part: str) -> str:
170
+ if path_part == 'root':
171
+ return '/'
172
+
173
+ display = path_part.replace('_', '/')
174
+
175
+ if not display.startswith('/'):
176
+ display = '/' + display
177
+
178
+ return display
179
+
180
+ def _resolve_location(self) -> Path:
181
+ location = os.getenv('PROFILING_DIR', '.profile')
182
+
183
+ if not Path(location).is_absolute():
184
+ base = getattr(settings, 'BASE_DIR', Path.cwd())
185
+ location = Path(base) / location
186
+ else:
187
+ location = Path(location)
188
+
189
+ return location
190
+
191
+ def _try_delete(self, filepath: Path) -> JsonResponse | None:
192
+ for i in range(3):
193
+ try:
194
+ filepath.unlink()
195
+ except FileNotFoundError:
196
+ return JsonResponse({'success': True, 'is_deleted': True})
197
+ except PermissionError:
198
+ if i < 2:
199
+ time.sleep(0.1)
200
+ continue
201
+
202
+ return JsonResponse(
203
+ {'error': 'File is in use, try again'},
204
+ status=409
205
+ )
206
+ except Exception as e:
207
+ return JsonResponse({'error': str(e)}, status=500)
208
+ else:
209
+ return JsonResponse({'success': True})
210
+
211
+ return None
212
+
213
+ def _validate_filename(self, filename: str) -> str | None:
214
+ if '..' in filename or '/' in filename or '\\' in filename:
215
+ return 'Invalid filename'
216
+
217
+ if not filename.endswith('.html'):
218
+ return 'Only HTML files are allowed'
219
+
220
+ return None
221
+
222
+ def _validate_filepath(self, filepath: Path, location: Path) -> str | None:
223
+ if not filepath.is_file():
224
+ return 'Not a file'
225
+
226
+ try:
227
+ filepath.resolve().relative_to(location.resolve())
228
+ except ValueError:
229
+ return 'Invalid file path'
230
+
231
+ return None
232
+
233
+ @classmethod
234
+ def delete_file(cls, request: HttpRequest, filename: str) -> JsonResponse:
235
+ panel = cls(None, None)
236
+ error = panel._validate_filename(filename)
237
+
238
+ if error:
239
+ return JsonResponse({'error': error}, status=400)
240
+
241
+ location = panel._resolve_location()
242
+ filepath = location / filename
243
+
244
+ with lock:
245
+ if not filepath.exists():
246
+ return JsonResponse({'success': True, 'is_deleted': True})
247
+
248
+ error = panel._validate_filepath(filepath, location)
249
+
250
+ if error:
251
+ return JsonResponse({'error': error}, status=400)
252
+
253
+ result = panel._try_delete(filepath)
254
+
255
+ if result:
256
+ return result
257
+
258
+ return JsonResponse({'error': 'Could not delete file'}, status=500)
259
+
260
+ def generate_stats(self, request: HttpRequest, response: HttpResponse) -> None:
261
+ profiles = self._get_files()
262
+ profiling = os.getenv('PROFILING_ENABLED', 'False') == 'True'
263
+
264
+ stats = ProfileStats(
265
+ profiles=[asdict(profile) for profile in profiles],
266
+ count=len(profiles),
267
+ directory=str(self.directory),
268
+ total_size=self._format_size(self._get_size()),
269
+ enabled=profiling
270
+ )
271
+
272
+ self.record_stats(asdict(stats))
273
+
274
+ @classmethod
275
+ def get_urls(cls) -> list:
276
+ return [
277
+ re_path(
278
+ r'^profiling/view/(?P<filename>[^/]+)/$',
279
+ cls.serve_file,
280
+ name='profiling_view'
281
+ ),
282
+ re_path(
283
+ r'^profiling/delete/(?P<filename>[^/]+)/$',
284
+ csrf_exempt(cls.delete_file),
285
+ name='profiling_delete'
286
+ ),
287
+ re_path(
288
+ r'^profiling/list/$',
289
+ cls.list_files,
290
+ name='profiling_list'
291
+ ),
292
+ ]
293
+
294
+ @classmethod
295
+ def list_files(cls, request: HttpRequest) -> JsonResponse:
296
+ panel = cls(None, None)
297
+ panel.directory = panel._resolve_location()
298
+ profiles = panel._get_files()
299
+
300
+ response = ProfileList(
301
+ profiles=[asdict(profile) for profile in profiles],
302
+ count=len(profiles),
303
+ total_size=panel._format_size(panel._get_size())
304
+ )
305
+
306
+ return JsonResponse(asdict(response))
307
+
308
+ @property
309
+ def nav_subtitle(self) -> str:
310
+ count = 0
311
+
312
+ if self.directory.exists():
313
+ with lock:
314
+ count = sum(
315
+ 1 for f in self.directory.glob('*.html')
316
+ if f.exists()
317
+ )
318
+
319
+ return f'{count} profiles'
320
+
321
+ @classmethod
322
+ def serve_file(cls, request: HttpRequest, filename: str) -> HttpResponse:
323
+ panel = cls(None, None)
324
+ error = panel._validate_filename(filename)
325
+
326
+ if error:
327
+ raise Http404(error)
328
+
329
+ location = panel._resolve_location()
330
+ filepath = location / filename
331
+
332
+ with lock:
333
+ if not filepath.exists():
334
+ message = 'Profile has been removed'
335
+ raise Http404(message)
336
+
337
+ error = panel._validate_filepath(filepath, location)
338
+
339
+ if error:
340
+ raise Http404(error)
341
+
342
+ with open(filepath, 'r', encoding='utf-8') as handle:
343
+ content = handle.read()
344
+
345
+ return HttpResponse(content, content_type='text/html')
@@ -0,0 +1,166 @@
1
+ {% load i18n %}
2
+
3
+ <style>
4
+ .djdt-profiling-panel {
5
+ padding: 10px;
6
+ }
7
+
8
+ .djdt-profiling-stats {
9
+ display: flex;
10
+ gap: 30px;
11
+ margin-bottom: 30px;
12
+ align-items: center;
13
+ padding-bottom: 20px;
14
+ border-bottom: 1px solid #e0e0e0;
15
+ }
16
+
17
+ .djdt-profiling-stat {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 10px;
21
+ }
22
+
23
+ .djdt-profiling-stat-label {
24
+ opacity: 0.7;
25
+ }
26
+
27
+ .djdt-profiling-stat-value {
28
+ font-weight: bold;
29
+ }
30
+
31
+ .djdt-profiling-table {
32
+ width: 100%;
33
+ border-collapse: collapse;
34
+ font-size: 0.9em;
35
+ }
36
+
37
+ .djdt-profiling-table th {
38
+ padding: 8px;
39
+ text-align: left;
40
+ font-weight: bold;
41
+ }
42
+
43
+ .djdt-profiling-table td {
44
+ padding: 8px;
45
+ }
46
+
47
+ .djdt-profile-link {
48
+ text-decoration: none;
49
+ font-weight: bold;
50
+ }
51
+
52
+ .djdt-profile-link:hover {
53
+ text-decoration: underline;
54
+ }
55
+
56
+ .djdt-profile-delete {
57
+ text-decoration: none;
58
+ font-weight: bold;
59
+ }
60
+
61
+ .djdt-profile-delete:hover {
62
+ text-decoration: underline;
63
+ }
64
+
65
+ .djdt-no-profiles {
66
+ padding: 20px;
67
+ text-align: center;
68
+ font-style: italic;
69
+ }
70
+
71
+ .djdt-circular-buffer-note {
72
+ padding: 8px;
73
+ border-radius: 3px;
74
+ margin-bottom: 10px;
75
+ font-size: 3.85em;
76
+ }
77
+ </style>
78
+
79
+ <div class="djdt-profiling-panel">
80
+ <div class="djdt-profiling-stats">
81
+ <div class="djdt-profiling-stat">
82
+ <div class="djdt-profiling-stat-label">Status:</div>
83
+ <div class="djdt-profiling-stat-value">
84
+ {% if enabled %}Enabled{% else %}Disabled{% endif %}
85
+ </div>
86
+ </div>
87
+
88
+ <div class="djdt-profiling-stat">
89
+ <div class="djdt-profiling-stat-label">Profiles:</div>
90
+ <div class="djdt-profiling-stat-value" id="profile-count">{{ count }}</div>
91
+ </div>
92
+
93
+ <div class="djdt-profiling-stat">
94
+ <div class="djdt-profiling-stat-label">Size:</div>
95
+ <div class="djdt-profiling-stat-value" id="total-size">{{ total_size }}</div>
96
+ </div>
97
+
98
+ <div class="djdt-profiling-stat">
99
+ <div class="djdt-profiling-stat-label">Directory:</div>
100
+ <div class="djdt-profiling-stat-value">{{ directory }}</div>
101
+ </div>
102
+
103
+ <div class="djdt-profiling-stat" style="margin-left: auto;">
104
+ <a href="#" onclick="if(confirm('Delete all profiles?')){let rows=document.querySelectorAll('#profiling-tbody tr');let promises=[];rows.forEach(row=>{let filename=row.dataset.filename;if(filename)promises.push(fetch('/__debug__/profiling/delete/'+filename+'/').then(r=>r.json()).catch(e=>({error:e.toString()})));});Promise.all(promises).then(results=>{let success_count=0;let is_deleted=0;let failures=0;results.forEach(r=>{if(r.success)success_count++;else if(r.is_deleted)is_deleted++;else failures++;});document.getElementById('profiling-tbody').innerHTML='';document.getElementById('profiling-table').outerHTML='<div class=djdt-no-profiles>No profiles found</div>';document.getElementById('profile-count').textContent='0';document.getElementById('total-size').textContent='0 B';let panels=document.querySelectorAll('.djDebugPanelButton');panels.forEach(p=>{if(p.textContent.includes('Profiles')){let small=p.querySelector('small');if(small)small.textContent='0 profiles';}});if(failures>0){alert('Deleted '+success_count+' profiles. '+failures+' failed (may have been removed by circular buffer).');}}).catch(e=>alert('Error during bulk delete: '+e));}return false;" style="color: #d32f2f; font-weight: bold; text-decoration: none;">
105
+ Delete All
106
+ </a>
107
+ </div>
108
+ </div>
109
+
110
+ <br>
111
+
112
+ <div class="djdt-circular-buffer-note">
113
+ <strong>Note:</strong> The ten most recent profiles are kept, and the remainder are removed.
114
+ </div>
115
+
116
+ <br>
117
+
118
+ {% if not enabled %}
119
+ <div style="padding: 10px; margin-bottom: 15px; font-size: 0.9em; opacity: 0.7;">
120
+ Set PROFILING_ENABLED=True to enable profiling
121
+ </div>
122
+ {% endif %}
123
+
124
+ {% if profiles %}
125
+ <table class="djdt-profiling-table" id="profiling-table">
126
+ <thead>
127
+ <tr>
128
+ <th>Method</th>
129
+ <th>Path</th>
130
+ <th>Duration</th>
131
+ <th>Time</th>
132
+ <th>Size</th>
133
+ <th>View</th>
134
+ <th>Delete</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody id="profiling-tbody">
138
+ {% for profile in profiles %}
139
+ <tr data-filename="{{ profile.filename }}">
140
+ <td>
141
+ <span>{{ profile.method }}</span>
142
+ </td>
143
+ <td>{{ profile.path }}</td>
144
+ <td>{{ profile.duration }}</td>
145
+ <td>{{ profile.modified }}</td>
146
+ <td>{{ profile.size }}</td>
147
+ <td>
148
+ <a href="{% url 'djdt:profiling_view' profile.filename %}" target="_blank" class="djdt-profile-link">
149
+ View
150
+ </a>
151
+ </td>
152
+ <td>
153
+ <a href="#" onclick="if(confirm('Delete this profile?')){fetch('/__debug__/profiling/delete/{{ profile.filename }}/').then(r=>r.json()).then(d=>{if(d.success||d.is_deleted){let row=this.closest('tr');row.remove();let count=document.querySelectorAll('#profiling-tbody tr').length;document.getElementById('profile-count').textContent=count;fetch('/__debug__/profiling/list/').then(r=>r.json()).then(data=>{document.getElementById('total-size').textContent=data.total_size;});let panels=document.querySelectorAll('.djDebugPanelButton');panels.forEach(p=>{if(p.textContent.includes('Profiles')){let small=p.querySelector('small');if(small)small.textContent=count+' profiles';}});if(count===0){document.getElementById('profiling-table').outerHTML='<div class=djdt-no-profiles>No profiles found</div>';}if(d.is_deleted){alert('Profile was already removed (circular buffer)');}}else if(d.error){alert('Failed to delete: '+d.error);}}).catch(e=>{console.error('Delete error:',e);alert('Network error while deleting');});}return false;" class="djdt-profile-delete">
154
+ Delete
155
+ </a>
156
+ </td>
157
+ </tr>
158
+ {% endfor %}
159
+ </tbody>
160
+ </table>
161
+ {% else %}
162
+ <div class="djdt-no-profiles">
163
+ No profiles found in {{ directory }}
164
+ </div>
165
+ {% endif %}
166
+ </div>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-spire
3
- Version: 0.16.13
3
+ Version: 0.17.0
4
4
  Summary: A project for Django Spire
5
5
  Author-email: Brayden Carlson <braydenc@stratusadv.com>, Nathan Johnson <nathanj@stratusadv.com>
6
6
  License: Copyright (c) 2024 Stratus Advanced Technologies and Contributors.
@@ -1,6 +1,6 @@
1
1
  django_spire/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  django_spire/conf.py,sha256=EYC1hXqYqheYrb_b6Q93LrSgNl91JV633E4N5j-KGTo,1012
3
- django_spire/consts.py,sha256=J6U3ROjjdihsXICCCn68i2QMWT14HbmGOO8dZ9agt9U,391
3
+ django_spire/consts.py,sha256=fGGLMXMV9HZCluwzPHZ9ijrXLb_jLrCjzG0u1BEIV3k,390
4
4
  django_spire/exceptions.py,sha256=L5ndRO5ftMmh0pHkO2z_NG3LSGZviJ-dDHNT73SzTNw,48
5
5
  django_spire/settings.py,sha256=tGxgEri3TQRBaiwX7YRcWifMf_g1xv1RznplFR6zZnI,821
6
6
  django_spire/urls.py,sha256=mKeZszb5U4iIGqddMb5Tt5fRC72U2wABEOi6mvOfEBU,656
@@ -408,25 +408,23 @@ django_spire/core/forms/widgets.py,sha256=_XgJZ9SoMJ4yz-NhLCAOppSvEWk_5JLUtn3I3_
408
408
  django_spire/core/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
409
409
  django_spire/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
410
410
  django_spire/core/management/commands/spire_remove_migration.py,sha256=G3dibVdzvEm9ZMfE_czOk6SyPDn5IEpJ3m1nw-rxuYs,1755
411
- django_spire/core/management/commands/spire_startapp.py,sha256=TWSN7c8-dgn0BBHn6pOq0oMBTxZ7keKBs3Yzu7WKfRo,3001
412
- django_spire/core/management/commands/spire_startapp_pkg/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
413
- django_spire/core/management/commands/spire_startapp_pkg/constants.py,sha256=PPlf_6xV2nQucGvGuqEHk64ZN8HEJiyot1Yst8jSY2A,94
414
- django_spire/core/management/commands/spire_startapp_pkg/manager.py,sha256=HWgPHtApsDDsw4o1NhboSdw40kr2Yc7WP5Grh9ippkQ,5251
415
- django_spire/core/management/commands/spire_startapp_pkg/maps.py,sha256=F2oYHMYII2x1sKMRHEc3hF8Rd3ftjIiIxYnYUALs49A,1140
416
- django_spire/core/management/commands/spire_startapp_pkg/processor.py,sha256=yEiGQMrgTOxvd-PAvmgFQjFIaAE-NBu80LD2CR5abfY,2623
417
- django_spire/core/management/commands/spire_startapp_pkg/reporter.py,sha256=alxyyH7zX7f0cFO-rKJq0pP9N7UfZIUGVx2XN9yh7bs,6489
411
+ django_spire/core/management/commands/spire_startapp.py,sha256=XAA3m7a-dKL4ZYO_WDzKW8ZDgB3__j2RAyiz6qllYjk,4572
412
+ django_spire/core/management/commands/spire_startapp_pkg/__init__.py,sha256=DN9Zllp-XszJawudgbcMKfhhTIPnjWzeTGUSRlwEaG4,1894
413
+ django_spire/core/management/commands/spire_startapp_pkg/builder.py,sha256=1rGZlHdnTM9QdTFFjDf87xCSCA2QIUd8ndtVvf-PbGE,3170
414
+ django_spire/core/management/commands/spire_startapp_pkg/config.py,sha256=qoIGKUYJcRAnYW1WbdpSxKerWckeJGxoDVCZ9yWxrgY,3390
415
+ django_spire/core/management/commands/spire_startapp_pkg/filesystem.py,sha256=shYly0AElxhUsb1WOsK8FdcB3qcsMmghZeEP1IByHq8,3719
416
+ django_spire/core/management/commands/spire_startapp_pkg/generator.py,sha256=MleLiKyFnILhCiQy_BcMRVCdw-1JOhB-7EZBMFdukKc,5989
417
+ django_spire/core/management/commands/spire_startapp_pkg/maps.py,sha256=m0SKcy_TZ4O-mjI74qkxyhVjoDN4QvMmQxgvbbn6mAE,22977
418
+ django_spire/core/management/commands/spire_startapp_pkg/permissions.py,sha256=czSE-ZNBw-3LsGrtkaD7CBFnitYX18KQjgJ_jjbap7k,5260
419
+ django_spire/core/management/commands/spire_startapp_pkg/processor.py,sha256=fqnbgaw8AsNHQBZnbDLmU4WPueFx5bQzA9Cwi_lD_KQ,5928
420
+ django_spire/core/management/commands/spire_startapp_pkg/registry.py,sha256=izf5SHkek2nIEf0UkODXL4uY1SkWXmoL3_s6pihF2QE,2689
421
+ django_spire/core/management/commands/spire_startapp_pkg/reporter.py,sha256=C48v1dLgnzmGijE9m_G_k5dMuOmNq3Am1zETjiEzEuQ,11085
422
+ django_spire/core/management/commands/spire_startapp_pkg/resolver.py,sha256=-y86EMmWU65bMzGeS5mI_O0iVu-xfVrdVKXtrOgbwUg,2835
423
+ django_spire/core/management/commands/spire_startapp_pkg/user_input.py,sha256=VUyY9PrqM07vRvgbVJhkC4lgNolnBik3K8S_8U9s_zM,9518
424
+ django_spire/core/management/commands/spire_startapp_pkg/validator.py,sha256=cVzQqaShGZ0ae-M69tHFDgXlYBmtR2Zg0zblIj55-Nc,3404
418
425
  django_spire/core/management/commands/spire_startapp_pkg/template/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
419
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_detail_card.html,sha256=mLq1OENpsrJQ7FiEAwowwE4ZlhcFZuwhfZPvYN3mZs8,953
420
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html,sha256=m-z23qNNn5R4ctuAypXoQgcti2mz-l57uvBmptBZG-E,233
421
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html,sha256=tJIZLL89SUV8As6RBRiYwIDVhEjgUu8173YRCDaRZHo,659
422
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html,sha256=fQ-kULFIGACH7YVWK2S4O3hEp1DwjumqxOgymp15e7E,686
423
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html,sha256=m_R9HlPcH89eSNcKkNwD8DXnuH7oBU3pw7YrENWgrKI,1244
424
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html,sha256=dOxIi0ISZwYpyPTrH1IuAJjWR4jWzgtpzaWpWYxSA3I,434
425
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html,sha256=9eSbAsMK8rjzSYmrq_NZOB6F1EphMIO4KIoDlv6YOrw,331
426
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html,sha256=SwTuxzNzTW6_nlZvrglSG2amithT0ZCjnNnrAd9aaA0,236
427
- django_spire/core/middleware/__init__.py,sha256=peocpyT_vXsjK3MXzpWIbsNVnHBbuyybH90R9V-lN1c,243
426
+ django_spire/core/middleware/__init__.py,sha256=BvBwVHLVebknJkObtKk_s5OqaIJaAmnavTvnnv4TBO8,149
428
427
  django_spire/core/middleware/maintenance.py,sha256=jJtmz69UXJBBgPkq5SNspWuER1bGfAjSnsVipYS4TF0,921
429
- django_spire/core/middleware/profiling.py,sha256=oW8S5oZQYq96a28cmVPLwyhz3xBAZHkTK7NMxLUDXCw,4807
430
428
  django_spire/core/redirect/__init__.py,sha256=ubAsz_h3ufwgNNkdtqiJQDgXc6DodE2LTUyBwi9dgkE,71
431
429
  django_spire/core/redirect/generic_redirect.py,sha256=bfpA2GSRbkf5Y_zqTiTGzakQauLumm3JbaYMzmsDhjA,1245
432
430
  django_spire/core/redirect/safe_redirect.py,sha256=deGLqiR1wWwqlJ8BYp76qDUDHnfRrxL-1Vns3nozSG0,2901
@@ -994,6 +992,11 @@ django_spire/notification/sms/urls/__init__.py,sha256=9R6ee7PseGpAiiraEU0m4eeF8G
994
992
  django_spire/notification/sms/urls/media_urls.py,sha256=fYAW8LWMwUe5nP2QuWWdnhuSyv-0PX4nIw0vjbabENQ,271
995
993
  django_spire/notification/sms/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
996
994
  django_spire/notification/sms/views/media_views.py,sha256=STrTC1irBSHEFQ0SZfUhpBI9Xmc6luWDf7Lembfd5Ic,1080
995
+ django_spire/profiling/__init__.py,sha256=V5zHoTaqGCOU-gfwkGQI01XcXeRYoAjO103yDkk1tIQ,251
996
+ django_spire/profiling/panel.py,sha256=BBISa2T35_QV2ODtx0DsL2ypK_BbETFSdr9iu_wIgCk,9570
997
+ django_spire/profiling/middleware/__init__.py,sha256=K6wjo7xUhOAJaB5Vbqa5JRf3EQM78NwToz_VtW3zz78,148
998
+ django_spire/profiling/middleware/profiling.py,sha256=Vl8WHCEYoUl1P50uFog2Gp6hK2INSRZC2t4LuY46DOA,5103
999
+ django_spire/profiling/templates/panel.html,sha256=YNK5ypTpUezWCtT0CE-awIpKPo4pKOPo9wbD4fkzgwc,6684
997
1000
  django_spire/theme/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
998
1001
  django_spire/theme/apps.py,sha256=yol2S7ooTuplc8JKETEg6oAu9hpeHUpJ6wt1Bo3dQ3E,479
999
1002
  django_spire/theme/enums.py,sha256=I0Sa5GzAntAPL8d7BJykhzuftA7ZweqZZtFIcH_g6kA,495
@@ -1043,8 +1046,8 @@ django_spire/theme/urls/page_urls.py,sha256=S8nkKkgbhG3XHI3uMUL-piOjXIrRkuY2UlM_
1043
1046
  django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1044
1047
  django_spire/theme/views/json_views.py,sha256=W1khC2K_EMbEzAFmMxC_P76_MFnkRH4-eVdodrRfAhw,1904
1045
1048
  django_spire/theme/views/page_views.py,sha256=pHr8iekjtR99xs7w1taj35HEo133Svq1dvDD0y0VL1c,3933
1046
- django_spire-0.16.13.dist-info/licenses/LICENSE.md,sha256=tlTbOtgKoy-xAQpUk9gPeh9O4oRXCOzoWdW3jJz0wnA,1091
1047
- django_spire-0.16.13.dist-info/METADATA,sha256=qkok6POcbtu3Fobg9hSCBCEceQU3mQaRWU3YM5moeNk,4937
1048
- django_spire-0.16.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1049
- django_spire-0.16.13.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1050
- django_spire-0.16.13.dist-info/RECORD,,
1049
+ django_spire-0.17.0.dist-info/licenses/LICENSE.md,sha256=tlTbOtgKoy-xAQpUk9gPeh9O4oRXCOzoWdW3jJz0wnA,1091
1050
+ django_spire-0.17.0.dist-info/METADATA,sha256=bVDHr6SnMBQaXRxcp9wExF6zN6mOQLORWWzrZDZr_WU,4936
1051
+ django_spire-0.17.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1052
+ django_spire-0.17.0.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1053
+ django_spire-0.17.0.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- INDENTATION = ' '
2
- ICON_FOLDER_OPEN = '📂'
3
- ICON_FOLDER_CLOSED = '📁'
4
- ICON_FILE = '📄'