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
@@ -1,41 +1,186 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Callable, TYPE_CHECKING
4
-
5
- from django_spire.core.management.commands.spire_startapp_pkg.constants import (
6
- INDENTATION,
7
- ICON_FOLDER_OPEN,
8
- ICON_FOLDER_CLOSED,
9
- ICON_FILE
10
- )
3
+ from string import Template
4
+ from typing import Protocol, TYPE_CHECKING
5
+
11
6
  from django_spire.core.management.commands.spire_startapp_pkg.maps import generate_replacement_map
12
7
 
13
8
  if TYPE_CHECKING:
14
9
  from pathlib import Path
10
+ from typing import Callable
15
11
 
16
12
  from django.core.management.base import BaseCommand
17
13
 
18
14
 
15
+ class ReporterInterface(Protocol):
16
+ """
17
+ Protocol defining the interface for reporting and user interaction.
18
+
19
+ This protocol specifies methods for displaying messages, prompts,
20
+ and tree structures to the user during app creation.
21
+ """
22
+
23
+ def prompt_confirmation(self, message: str) -> bool: ...
24
+ def report_app_creation_success(self, app: str) -> None: ...
25
+ def report_app_exists(self, app: str, destination: Path) -> None: ...
26
+ def report_creating_app(self, app: str, destination: Path) -> None: ...
27
+ def report_creating_templates(self, app: str, destination: Path) -> None: ...
28
+ def report_installed_apps_suggestion(self, missing_components: list[str]) -> None: ...
29
+ def report_missing_components(self, missing_components: list[str]) -> None: ...
30
+ def report_templates_creation_success(self, app: str) -> None: ...
31
+ def report_templates_exist(self, app: str, destination: Path) -> None: ...
32
+ def write(self, message: str, style: Callable[[str], str]) -> None: ...
33
+
34
+
19
35
  class Reporter:
36
+ """
37
+ Handles user interaction and console output for the app creation command.
38
+
39
+ This class manages all console output including status messages, tree
40
+ structures, confirmations, and styled text for the app creation process.
41
+ """
42
+
43
+ ICON_FILE = '📄'
44
+ ICON_FOLDER_CLOSED = '📁'
45
+ ICON_FOLDER_OPEN = '📂'
46
+ INDENTATION = ' '
47
+
20
48
  def __init__(self, command: BaseCommand):
49
+ """
50
+ Initializes the reporter with a Django management command.
51
+
52
+ :param command: Django BaseCommand instance for accessing stdout and styling.
53
+ """
54
+
21
55
  self.command = command
56
+ self.style_error = command.style.ERROR
57
+ self.style_notice = command.style.NOTICE
58
+ self.style_success = command.style.SUCCESS
59
+ self.style_warning = command.style.WARNING
22
60
 
23
- def _apply_replacement(self, name: str, replacement: dict[str, str]) -> str:
24
- for old, new in replacement.items():
25
- name = name.replace(old, new)
61
+ def format_app_item(self, item: Path) -> str:
62
+ """
63
+ Formats an app file or directory name for display.
26
64
 
27
- return name
65
+ Removes the .template extension from Python files.
28
66
 
29
- def _sort_template_items(self, path: Path) -> tuple[bool, str]:
30
- return (path.is_file(), path.name.lower())
67
+ :param item: Path to the item to format.
68
+ :return: Formatted item name.
69
+ """
31
70
 
32
- def _app_transformation(self, _index: int, component: str) -> str:
33
- return component
71
+ return item.name.replace('.py.template', '.py')
34
72
 
35
- def _html_transformation(self, index: int, component: str) -> str:
36
- return 'templates' if index == 0 else component
73
+ def format_html_item(self, item: Path, replacement: dict[str, str]) -> str:
74
+ """
75
+ Formats an HTML template file or directory name for display.
76
+
77
+ Applies variable replacements and removes .template extensions.
78
+
79
+ :param item: Path to the item to format.
80
+ :param replacement: Dictionary of placeholder replacements.
81
+ :return: Formatted item name with placeholders replaced.
82
+ """
83
+
84
+ if item.is_dir():
85
+ return item.name
86
+
87
+ template = Template(item.name)
88
+ filename = template.safe_substitute(replacement)
89
+
90
+ if filename.endswith('.template'):
91
+ filename = filename.replace('.template', '')
92
+
93
+ return filename
94
+
95
+ def prompt_confirmation(self, message: str) -> bool:
96
+ """
97
+ Prompts the user for yes/no confirmation.
98
+
99
+ :param message: Confirmation prompt message.
100
+ :return: True if user responds with 'y', False otherwise.
101
+ """
102
+
103
+ return input(message).strip().lower() == 'y'
104
+
105
+ def report_app_creation_success(self, app: str) -> None:
106
+ """
107
+ Reports successful creation of an app.
108
+
109
+ :param app: Name of the app that was created.
110
+ """
111
+
112
+ self.write(f'Successfully created app: {app}', self.style_success)
113
+
114
+ def report_app_exists(self, app: str, destination: Path) -> None:
115
+ """
116
+ Reports that an app already exists at the destination.
117
+
118
+ :param app: Name of the app.
119
+ :param destination: Path where the app already exists.
120
+ """
121
+
122
+ self.write(f'The app "{app}" already exists at {destination}', self.style_warning)
123
+
124
+ def report_creating_app(self, app: str, destination: Path) -> None:
125
+ """
126
+ Reports that an app is being created.
127
+
128
+ :param app: Name of the app being created.
129
+ :param destination: Path where the app will be created.
130
+ """
131
+
132
+ self.write(f'Creating app "{app}" at {destination}', self.style_notice)
133
+
134
+ def report_creating_templates(self, app: str, destination: Path) -> None:
135
+ """
136
+ Reports that templates are being created for an app.
137
+
138
+ :param app: Name of the app.
139
+ :param destination: Path where templates will be created.
140
+ """
141
+
142
+ self.write(f'Creating templates for app "{app}" at {destination}', self.style_notice)
143
+
144
+ def report_installed_apps_suggestion(self, missing_components: list[str]) -> None:
145
+ """
146
+ Suggests which app to add to INSTALLED_APPS in settings.py.
147
+
148
+ :param missing_components: List of missing app components.
149
+ """
150
+
151
+ self.write('\nPlease add the following to INSTALLED_APPS in settings.py:', self.style_notice)
152
+ self.write(f'\n {missing_components[-1]}', lambda x: x)
153
+
154
+ def report_missing_components(self, missing_components: list[str]) -> None:
155
+ """
156
+ Reports which app components are not yet registered.
157
+
158
+ :param missing_components: List of unregistered app component paths.
159
+ """
160
+
161
+ self.write('The following are not registered apps:', self.style_warning)
162
+ self.write('\n'.join(f' - {app}' for app in missing_components), lambda x: x)
163
+
164
+ def report_templates_creation_success(self, app: str) -> None:
165
+ """
166
+ Reports successful creation of templates for an app.
167
+
168
+ :param app: Name of the app whose templates were created.
169
+ """
170
+
171
+ self.write(f'Successfully created templates for app: {app}', self.style_success)
172
+
173
+ def report_templates_exist(self, app: str, destination: Path) -> None:
174
+ """
175
+ Reports that templates already exist for an app.
176
+
177
+ :param app: Name of the app.
178
+ :param destination: Path where templates already exist.
179
+ """
37
180
 
38
- def _report_tree_structure(
181
+ self.write(f'The templates for app "{app}" already exist at {destination}', self.style_warning)
182
+
183
+ def report_tree_structure(
39
184
  self,
40
185
  title: str,
41
186
  base: Path,
@@ -45,8 +190,23 @@ class Reporter:
45
190
  formatter: Callable[[Path], str],
46
191
  transformation: Callable[[int, str], str] | None = None,
47
192
  ) -> None:
193
+ """
194
+ Displays a tree structure of files and directories that will be created.
195
+
196
+ Shows a hierarchical view of the app structure with appropriate icons
197
+ and formatting for files and directories.
198
+
199
+ :param title: Title to display above the tree structure.
200
+ :param base: Base directory path.
201
+ :param components: List of app path components.
202
+ :param registry: List of registered apps.
203
+ :param template: Path to template directory.
204
+ :param formatter: Function to format item names for display.
205
+ :param transformation: Optional function to transform component names.
206
+ """
207
+
48
208
  if transformation is None:
49
- transformation = self._app_transformation
209
+ transformation = self.transform_app_component
50
210
 
51
211
  self.command.stdout.write(title)
52
212
  current = base
@@ -58,16 +218,61 @@ class Reporter:
58
218
  component = transformation(i, component)
59
219
  current = current / component
60
220
  app = '.'.join(latest)
61
- indent = INDENTATION * i
221
+ indent = self.INDENTATION * i
62
222
 
63
- self.command.stdout.write(f'{indent}{ICON_FOLDER_OPEN} {component}/')
223
+ self.command.stdout.write(f'{indent}{self.ICON_FOLDER_OPEN} {component}/')
64
224
 
65
225
  if i == len(components) - 1 and app not in registry and template.exists():
66
226
  def local_formatter(item: Path, mapping: dict[str, str] = replacement) -> str:
67
227
  base_name = formatter(item)
68
228
  return self._apply_replacement(base_name, mapping)
69
229
 
70
- self._show_tree_from_template(template, indent + INDENTATION, local_formatter)
230
+ self._show_tree_from_template(template, indent + self.INDENTATION, local_formatter)
231
+
232
+ def transform_app_component(self, _index: int, component: str) -> str:
233
+ """
234
+ Transforms an app component name (default: no transformation).
235
+
236
+ :param _index: Index of the component in the path.
237
+ :param component: Component name to transform.
238
+ :return: Transformed component name.
239
+ """
240
+
241
+ return component
242
+
243
+ def transform_html_component(self, index: int, component: str) -> str:
244
+ """
245
+ Transforms an HTML component name (replaces first component with 'templates').
246
+
247
+ :param index: Index of the component in the path.
248
+ :param component: Component name to transform.
249
+ :return: Transformed component name.
250
+ """
251
+
252
+ return 'templates' if index == 0 else component
253
+
254
+ def write(self, message: str, style: Callable[[str], str]) -> None:
255
+ """
256
+ Writes a styled message to the console.
257
+
258
+ :param message: Message to display.
259
+ :param style: Styling function to apply to the message.
260
+ """
261
+
262
+ self.command.stdout.write(style(message))
263
+
264
+ def _apply_replacement(self, name: str, replacement: dict[str, str]) -> str:
265
+ """
266
+ Applies placeholder replacements to a name string.
267
+
268
+ :param name: String containing placeholders.
269
+ :param replacement: Dictionary mapping placeholders to values.
270
+ :return: String with placeholders replaced.
271
+ """
272
+
273
+ for old, new in replacement.items():
274
+ name = name.replace(old, new)
275
+ return name
71
276
 
72
277
  def _show_tree_from_template(
73
278
  self,
@@ -75,6 +280,14 @@ class Reporter:
75
280
  indent: str,
76
281
  formatter: Callable[[Path], str]
77
282
  ) -> None:
283
+ """
284
+ Recursively displays a tree structure from a template directory.
285
+
286
+ :param template: Template directory path to display.
287
+ :param indent: Current indentation level.
288
+ :param formatter: Function to format item names.
289
+ """
290
+
78
291
  ignore = {'__init__.py', '__pycache__'}
79
292
 
80
293
  items = sorted(template.iterdir(), key=self._sort_template_items)
@@ -83,98 +296,22 @@ class Reporter:
83
296
  if item.name in ignore:
84
297
  continue
85
298
 
86
- icon = ICON_FOLDER_CLOSED if item.is_dir() else ICON_FILE
299
+ icon = self.ICON_FOLDER_CLOSED if item.is_dir() else self.ICON_FILE
87
300
  self.command.stdout.write(f'{indent}{icon} {formatter(item)}')
88
301
 
89
302
  if item.is_dir():
90
303
  self._show_tree_from_template(
91
304
  item,
92
- indent + INDENTATION,
305
+ indent + self.INDENTATION,
93
306
  formatter
94
307
  )
95
308
 
96
- def _app_formatter(self, item: Path) -> str:
97
- return item.name.replace('.py.template', '.py')
98
-
99
- def _html_formatter(self, item: Path, replacement: dict[str, str]) -> str:
100
- if item.is_dir():
101
- return item.name
102
-
103
- return self._apply_replacement(item.name, replacement)
104
-
105
- def report_app_tree_structure(
106
- self,
107
- base: Path,
108
- components: list[str],
109
- registry: list[str],
110
- template: Path
111
- ) -> None:
112
- self._report_tree_structure(
113
- title='\nThe following app(s) will be created:\n\n',
114
- base=base,
115
- components=components,
116
- registry=registry,
117
- template=template,
118
- formatter=self._app_formatter,
119
- transformation=self._app_transformation,
120
- )
121
-
122
- def report_html_tree_structure(
123
- self,
124
- base: Path,
125
- components: list[str],
126
- registry: list[str],
127
- template: Path
128
- ) -> None:
129
- replacement = generate_replacement_map(components)
130
-
131
- def html_formatter_with_replacement(item: Path) -> str:
132
- return self._html_formatter(item, replacement)
133
-
134
- self._report_tree_structure(
135
- title='\nThe following template(s) will be created:\n\n',
136
- base=base,
137
- components=components,
138
- registry=registry,
139
- template=template,
140
- formatter=html_formatter_with_replacement,
141
- transformation=self._html_transformation,
142
- )
143
-
144
- def prompt_for_confirmation(self, message: str) -> bool:
145
- return input(message).strip().lower() == 'y'
146
-
147
- def report_missing_components(self, missing_components: list[str]) -> None:
148
- self.command.stdout.write(self.command.style.WARNING('The following are not registered apps:'))
149
- self.command.stdout.write('\n'.join(f' - {app}' for app in missing_components))
150
-
151
- def report_installed_apps_suggestion(self, missing_components: list[str]) -> None:
152
- self.command.stdout.write(self.command.style.NOTICE('\nPlease add the following to INSTALLED_APPS in settings.py:'))
153
- self.command.stdout.write(f'\n {missing_components[-1]}')
154
-
155
- def report_app_creation_success(self, app: str) -> None:
156
- message = f'Successfully created app: {app}'
157
- self.write(message, self.command.style.SUCCESS)
158
-
159
- def report_app_exists(self, app: str, destination: Path) -> None:
160
- message = f'The app "{app}" already exists at {destination}'
161
- self.write(message, self.command.style.WARNING)
162
-
163
- def report_creating_app(self, app: str, destination: Path) -> None:
164
- message = f'Creating app "{app}" at {destination}'
165
- self.write(message, self.command.style.NOTICE)
166
-
167
- def report_templates_exist(self, app: str, destination: Path) -> None:
168
- message = f'The templates for app "{app}" already exist at {destination}'
169
- self.write(message, self.command.style.WARNING)
170
-
171
- def report_creating_templates(self, app: str, destination: Path) -> None:
172
- message = f'Creating templates for app "{app}" at {destination}'
173
- self.write(message, self.command.style.NOTICE)
309
+ def _sort_template_items(self, path: Path) -> tuple[bool, str]:
310
+ """
311
+ Sorts template items with directories first, then by name.
174
312
 
175
- def report_templates_creation_success(self, app: str) -> None:
176
- message = f'Successfully created templates for app: {app}'
177
- self.write(message, self.command.style.SUCCESS)
313
+ :param path: Path to sort.
314
+ :return: Tuple for sorting (is_file, lowercase_name).
315
+ """
178
316
 
179
- def write(self, message: str, func: Callable[[str], str]) -> None:
180
- self.command.stdout.write(func(message))
317
+ return (path.is_file(), path.name.lower())
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+
6
+ from django.conf import settings
7
+
8
+
9
+ class PathResolverInterface(Protocol):
10
+ """
11
+ Protocol defining the interface for resolving file system paths.
12
+
13
+ This protocol specifies methods for determining where apps and
14
+ templates should be created in the file system.
15
+ """
16
+
17
+ def get_app_destination(self, components: list[str]) -> Path: ...
18
+ def get_base_dir(self) -> Path: ...
19
+ def get_template_destination(self, components: list[str]) -> Path: ...
20
+ def get_template_dir(self) -> Path: ...
21
+
22
+
23
+ class PathResolver:
24
+ """
25
+ Resolves file system paths for app and template creation.
26
+
27
+ This class determines where new Django apps and their templates
28
+ should be created based on project structure and configuration.
29
+ """
30
+
31
+ def __init__(self, base_dir: Path | None = None, template_dir: Path | None = None):
32
+ """
33
+ Initializes the path resolver with base directories.
34
+
35
+ :param base_dir: Optional base directory for the Django project (defaults to settings.BASE_DIR).
36
+ :param template_dir: Optional template directory (defaults to base_dir/templates).
37
+ """
38
+
39
+ self._base_dir = base_dir or Path(settings.BASE_DIR)
40
+ self._template_dir = template_dir or self._base_dir / 'templates'
41
+
42
+ def get_app_destination(self, components: list[str]) -> Path:
43
+ """
44
+ Gets the destination path for a new app based on its components.
45
+
46
+ For components ['app', 'human_resource', 'employee'], returns
47
+ Path('base_dir/app/human_resource/employee').
48
+
49
+ :param components: List of app path components.
50
+ :return: Full path where the app should be created.
51
+ """
52
+
53
+ return self._base_dir.joinpath(*components)
54
+
55
+ def get_base_dir(self) -> Path:
56
+ """
57
+ Gets the project's base directory.
58
+
59
+ :return: Base directory path for the Django project.
60
+ """
61
+
62
+ return self._base_dir
63
+
64
+ def get_template_destination(self, components: list[str]) -> Path:
65
+ """
66
+ Gets the destination path for templates based on app components.
67
+
68
+ Excludes the first component (root app) from the path. For components
69
+ ['app', 'human_resource', 'employee'], returns
70
+ Path('templates/human_resource/employee').
71
+
72
+ :param components: List of app path components.
73
+ :return: Full path where templates should be created.
74
+ """
75
+
76
+ template_components = components[1:] if len(components) > 1 else components
77
+ return self._template_dir.joinpath(*template_components)
78
+
79
+ def get_template_dir(self) -> Path:
80
+ """
81
+ Gets the project's template directory.
82
+
83
+ :return: Template directory path for the Django project.
84
+ """
85
+
86
+ return self._template_dir