django-spire 0.16.12__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 (170) hide show
  1. django_spire/ai/admin.py +3 -1
  2. django_spire/ai/apps.py +2 -0
  3. django_spire/ai/chat/admin.py +15 -9
  4. django_spire/ai/chat/apps.py +4 -1
  5. django_spire/ai/chat/auth/controller.py +3 -1
  6. django_spire/ai/chat/choices.py +2 -0
  7. django_spire/ai/chat/intelligence/maps/intent_llm_map.py +8 -5
  8. django_spire/ai/chat/intelligence/prompts.py +4 -2
  9. django_spire/ai/chat/intelligence/workflows/chat_workflow.py +27 -28
  10. django_spire/ai/chat/message_intel.py +7 -4
  11. django_spire/ai/chat/models.py +8 -9
  12. django_spire/ai/chat/querysets.py +3 -1
  13. django_spire/ai/chat/responses.py +19 -10
  14. django_spire/ai/chat/tools.py +20 -15
  15. django_spire/ai/chat/urls/message_urls.py +2 -1
  16. django_spire/ai/chat/urls/page_urls.py +1 -0
  17. django_spire/ai/chat/views/message_request_views.py +2 -0
  18. django_spire/ai/chat/views/message_response_views.py +4 -4
  19. django_spire/ai/chat/views/message_views.py +2 -0
  20. django_spire/ai/chat/views/page_views.py +7 -2
  21. django_spire/ai/chat/views/template_views.py +2 -0
  22. django_spire/ai/decorators.py +13 -7
  23. django_spire/ai/mixins.py +4 -2
  24. django_spire/ai/models.py +7 -2
  25. django_spire/ai/prompt/bots.py +14 -32
  26. django_spire/ai/prompt/intel.py +1 -1
  27. django_spire/ai/prompt/prompts.py +7 -1
  28. django_spire/ai/prompt/system/bots.py +42 -75
  29. django_spire/ai/prompt/system/intel.py +5 -4
  30. django_spire/ai/prompt/system/prompts.py +5 -1
  31. django_spire/ai/prompt/system/system_prompt_cli.py +15 -9
  32. django_spire/ai/prompt/tests/test_bots.py +14 -11
  33. django_spire/ai/prompt/text_to_prompt_cli.py +5 -2
  34. django_spire/ai/prompt/tuning/bot_tuning_cli.py +14 -13
  35. django_spire/ai/prompt/tuning/bots.py +68 -116
  36. django_spire/ai/prompt/tuning/intel.py +1 -1
  37. django_spire/ai/prompt/tuning/mixins.py +2 -0
  38. django_spire/ai/prompt/tuning/prompt_tuning_cli.py +8 -8
  39. django_spire/ai/prompt/tuning/prompts.py +4 -2
  40. django_spire/ai/sms/admin.py +3 -1
  41. django_spire/ai/sms/apps.py +2 -0
  42. django_spire/ai/sms/decorators.py +2 -0
  43. django_spire/ai/sms/intel.py +4 -2
  44. django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +8 -8
  45. django_spire/ai/sms/models.py +16 -14
  46. django_spire/ai/sms/querysets.py +4 -1
  47. django_spire/ai/sms/tools.py +18 -16
  48. django_spire/ai/sms/urls.py +1 -1
  49. django_spire/ai/sms/views.py +2 -0
  50. django_spire/ai/tests/test_ai.py +3 -5
  51. django_spire/ai/urls.py +1 -0
  52. django_spire/consts.py +1 -1
  53. django_spire/contrib/seeding/field/django/seeder.py +6 -4
  54. django_spire/contrib/seeding/field/llm.py +1 -2
  55. django_spire/contrib/seeding/intelligence/bots/field_seeding_bots.py +7 -8
  56. django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +15 -16
  57. django_spire/contrib/seeding/intelligence/intel.py +1 -1
  58. django_spire/contrib/seeding/intelligence/prompts/factory.py +5 -7
  59. django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +3 -5
  60. django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +1 -2
  61. django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +3 -5
  62. django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +1 -1
  63. django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +3 -3
  64. django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +1 -1
  65. django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +2 -4
  66. django_spire/contrib/seeding/management/commands/seeding.py +5 -2
  67. django_spire/contrib/seeding/model/base.py +12 -10
  68. django_spire/contrib/seeding/model/django/seeder.py +13 -10
  69. django_spire/contrib/seeding/tests/test_seeding.py +1 -1
  70. django_spire/core/management/commands/spire_startapp.py +84 -46
  71. django_spire/core/management/commands/spire_startapp_pkg/__init__.py +60 -0
  72. django_spire/core/management/commands/spire_startapp_pkg/builder.py +91 -0
  73. django_spire/core/management/commands/spire_startapp_pkg/config.py +115 -0
  74. django_spire/core/management/commands/spire_startapp_pkg/filesystem.py +125 -0
  75. django_spire/core/management/commands/spire_startapp_pkg/generator.py +167 -0
  76. django_spire/core/management/commands/spire_startapp_pkg/maps.py +783 -25
  77. django_spire/core/management/commands/spire_startapp_pkg/permissions.py +147 -0
  78. django_spire/core/management/commands/spire_startapp_pkg/processor.py +144 -57
  79. django_spire/core/management/commands/spire_startapp_pkg/registry.py +89 -0
  80. django_spire/core/management/commands/spire_startapp_pkg/reporter.py +245 -108
  81. django_spire/core/management/commands/spire_startapp_pkg/resolver.py +86 -0
  82. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +252 -0
  83. django_spire/core/management/commands/spire_startapp_pkg/validator.py +96 -0
  84. django_spire/core/middleware/__init__.py +1 -2
  85. django_spire/knowledge/admin.py +2 -0
  86. django_spire/knowledge/apps.py +2 -0
  87. django_spire/knowledge/auth/controller.py +2 -0
  88. django_spire/knowledge/collection/admin.py +3 -0
  89. django_spire/knowledge/collection/forms.py +2 -0
  90. django_spire/knowledge/collection/models.py +4 -0
  91. django_spire/knowledge/collection/querysets.py +3 -3
  92. django_spire/knowledge/collection/seeding/seed.py +2 -0
  93. django_spire/knowledge/collection/tests/factories.py +2 -0
  94. django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +2 -0
  95. django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +2 -0
  96. django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +2 -0
  97. django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +2 -0
  98. django_spire/knowledge/collection/views/json_views.py +2 -0
  99. django_spire/knowledge/collection/views/page_views.py +2 -0
  100. django_spire/knowledge/context_processors.py +2 -0
  101. django_spire/knowledge/entry/services/tool_service.py +1 -0
  102. django_spire/knowledge/entry/services/transformation_services.py +0 -1
  103. django_spire/knowledge/entry/tests/factories.py +3 -0
  104. django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +2 -0
  105. django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +2 -0
  106. django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +2 -0
  107. django_spire/knowledge/entry/urls/form_urls.py +1 -0
  108. django_spire/knowledge/entry/urls/json_urls.py +1 -0
  109. django_spire/knowledge/entry/urls/page_urls.py +1 -0
  110. django_spire/knowledge/entry/urls/template_urls.py +1 -0
  111. django_spire/knowledge/entry/version/admin.py +2 -0
  112. django_spire/knowledge/entry/version/block/admin.py +2 -0
  113. django_spire/knowledge/entry/version/block/blocks/heading_block.py +2 -0
  114. django_spire/knowledge/entry/version/block/blocks/sub_heading_block.py +2 -0
  115. django_spire/knowledge/entry/version/block/blocks/text_block.py +2 -0
  116. django_spire/knowledge/entry/version/block/maps.py +2 -0
  117. django_spire/knowledge/entry/version/block/models.py +2 -0
  118. django_spire/knowledge/entry/version/block/tests/factories.py +2 -0
  119. django_spire/knowledge/entry/version/block/tests/test_urls/test_json_urls.py +2 -0
  120. django_spire/knowledge/entry/version/block/views/json_views.py +2 -0
  121. django_spire/knowledge/entry/version/intelligence/bots/markdown_format_llm_bot.py +12 -9
  122. django_spire/knowledge/entry/version/maps.py +2 -0
  123. django_spire/knowledge/entry/version/models.py +2 -0
  124. django_spire/knowledge/entry/version/querysets.py +2 -0
  125. django_spire/knowledge/entry/version/seeding/seeder.py +1 -0
  126. django_spire/knowledge/entry/version/tests/factories.py +2 -0
  127. django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +3 -0
  128. django_spire/knowledge/entry/version/tests/test_urls/test_form_urls.py +2 -0
  129. django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +2 -0
  130. django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +2 -0
  131. django_spire/knowledge/entry/version/urls/form_urls.py +1 -0
  132. django_spire/knowledge/entry/version/urls/json_urls.py +1 -0
  133. django_spire/knowledge/entry/version/urls/page_urls.py +1 -0
  134. django_spire/knowledge/entry/version/urls/redirect_urls.py +1 -0
  135. django_spire/knowledge/entry/version/views/form_views.py +2 -0
  136. django_spire/knowledge/entry/version/views/json_views.py +2 -0
  137. django_spire/knowledge/entry/version/views/page_views.py +2 -0
  138. django_spire/knowledge/entry/version/views/redirect_views.py +2 -0
  139. django_spire/knowledge/exceptions.py +2 -0
  140. django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py +5 -16
  141. django_spire/knowledge/intelligence/intel/collection_intel.py +3 -1
  142. django_spire/knowledge/intelligence/intel/entry_intel.py +3 -3
  143. django_spire/knowledge/intelligence/intel/message_intel.py +2 -0
  144. django_spire/knowledge/intelligence/maps/collection_map.py +9 -10
  145. django_spire/knowledge/intelligence/maps/entry_map.py +8 -9
  146. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +8 -6
  147. django_spire/knowledge/models.py +2 -0
  148. django_spire/knowledge/seeding/seed.py +2 -0
  149. django_spire/knowledge/templatetags/spire_knowledge_tags.py +3 -0
  150. django_spire/knowledge/urls/__init__.py +1 -0
  151. django_spire/profiling/__init__.py +13 -0
  152. django_spire/profiling/middleware/__init__.py +6 -0
  153. django_spire/{core → profiling}/middleware/profiling.py +63 -58
  154. django_spire/profiling/panel.py +345 -0
  155. django_spire/profiling/templates/panel.html +166 -0
  156. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/METADATA +5 -4
  157. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/RECORD +160 -157
  158. django_spire/core/management/commands/spire_startapp_pkg/constants.py +0 -4
  159. django_spire/core/management/commands/spire_startapp_pkg/manager.py +0 -176
  160. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_detail_card.html +0 -24
  161. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html +0 -9
  162. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html +0 -18
  163. django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html +0 -22
  164. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html +0 -24
  165. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html +0 -13
  166. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html +0 -13
  167. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html +0 -9
  168. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/WHEEL +0 -0
  169. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from typing import Any
7
+
8
+ from django_spire.core.management.commands.spire_startapp_pkg.reporter import Reporter
9
+
10
+
11
+ class PermissionInheritanceHandler:
12
+ """
13
+ Handles permission inheritance configuration for nested Django apps.
14
+
15
+ This class manages the interactive collection of permission inheritance
16
+ settings when creating child apps that may inherit permissions from parent apps.
17
+ """
18
+
19
+ def __init__(self, reporter: Reporter):
20
+ """
21
+ Initializes the handler with a reporter for user interaction.
22
+
23
+ :param reporter: Reporter instance for displaying prompts and messages.
24
+ """
25
+
26
+ self.reporter = reporter
27
+
28
+ def collect_inheritance_data(self, components: list[str]) -> dict[str, Any]:
29
+ """
30
+ Collects permission inheritance configuration from the user.
31
+
32
+ Prompts the user to determine if the new app should inherit permissions
33
+ from its parent app, and if so, collects the necessary parent model information.
34
+
35
+ :param components: List of app path components.
36
+ :return: Dictionary containing inheritance configuration data.
37
+ """
38
+
39
+ if len(components) <= 2:
40
+ return {
41
+ 'inherit_permissions': False,
42
+ 'parent_model_instance_name': '',
43
+ 'parent_permission_prefix': '',
44
+ }
45
+
46
+ if not self._should_inherit_permissions():
47
+ return {
48
+ 'inherit_permissions': False,
49
+ 'parent_model_instance_name': '',
50
+ 'parent_permission_prefix': '',
51
+ }
52
+
53
+ return {
54
+ 'inherit_permissions': True,
55
+ 'parent_model_instance_name': self._collect_parent_model_instance_name(components),
56
+ 'parent_model_path': self._collect_parent_model_path(components),
57
+ 'parent_permission_prefix': self._collect_parent_permission_prefix(components),
58
+ }
59
+
60
+ def _build_default_parent_model_path(self, components: list[str]) -> str:
61
+ """
62
+ Builds a default parent model path based on app components.
63
+
64
+ :param components: List of app path components.
65
+ :return: Default parent model path string.
66
+ """
67
+
68
+ parent_name = components[-2]
69
+ parent_model_class = ''.join(word.title() for word in parent_name.split('_'))
70
+ return '.'.join(components[:-1]) + f'.models.{parent_model_class}'
71
+
72
+ def _build_default_parent_permission_prefix(self, components: list[str]) -> str:
73
+ """
74
+ Builds a default parent permission prefix based on app components.
75
+
76
+ :param components: List of app path components.
77
+ :return: Default permission prefix string.
78
+ """
79
+
80
+ parent_parts = components[1:-1]
81
+ return '_'.join(parent_parts).lower()
82
+
83
+ def _collect_parent_model_instance_name(self, components: list[str]) -> str:
84
+ """
85
+ Prompts the user for the parent model instance name.
86
+
87
+ :param components: List of app path components.
88
+ :return: User-provided or default parent model instance name.
89
+ """
90
+
91
+ parent_name = components[-2]
92
+ default = parent_name.lower()
93
+
94
+ self.reporter.write(
95
+ f'\nEnter the parent model instance name (default: "{default}")',
96
+ self.reporter.style_notice
97
+ )
98
+
99
+ user_input = input('Press Enter to use default or type a custom name: ').strip()
100
+ return user_input if user_input else default
101
+
102
+ def _collect_parent_model_path(self, components: list[str]) -> str:
103
+ """
104
+ Prompts the user for the parent model path.
105
+
106
+ :param components: List of app path components.
107
+ :return: User-provided or default parent model path.
108
+ """
109
+
110
+ default = self._build_default_parent_model_path(components)
111
+
112
+ self.reporter.write(
113
+ f'\nEnter the parent model path (default: "{default}")',
114
+ self.reporter.style_notice
115
+ )
116
+
117
+ user_input = input('Press Enter to use default or type a custom path: ').strip()
118
+ return user_input if user_input else default
119
+
120
+ def _collect_parent_permission_prefix(self, components: list[str]) -> str:
121
+ """
122
+ Prompts the user for the parent permission prefix.
123
+
124
+ :param components: List of app path components.
125
+ :return: User-provided or default parent permission prefix.
126
+ """
127
+
128
+ default = self._build_default_parent_permission_prefix(components)
129
+
130
+ self.reporter.write(
131
+ f'\nEnter the parent permission prefix (default: "{default}")',
132
+ self.reporter.style_notice
133
+ )
134
+
135
+ user_input = input('Press Enter to use default or type a custom prefix: ').strip()
136
+ return user_input if user_input else default
137
+
138
+ def _should_inherit_permissions(self) -> bool:
139
+ """
140
+ Prompts the user to confirm permission inheritance.
141
+
142
+ :return: True if user wants to inherit permissions, False otherwise.
143
+ """
144
+
145
+ self.reporter.write('\nDo you want this app to inherit permissions from its parent? (y/n)', self.reporter.style_notice)
146
+ user_input = input('Default is "n": ').strip().lower()
147
+ return user_input == 'y'
@@ -1,84 +1,171 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Callable, TYPE_CHECKING
3
+ from string import Template
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from django_spire.core.management.commands.spire_startapp_pkg.maps import generate_replacement_map
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from pathlib import Path
9
10
 
11
+ from django_spire.core.management.commands.spire_startapp_pkg.filesystem import FileSystemInterface
10
12
 
11
- class BaseTemplateProcessor:
12
- @staticmethod
13
- def apply_replacement(text: str, replacements: dict[str, str]) -> str:
14
- for old, new in replacements.items():
15
- text = text.replace(old, new)
16
13
 
17
- return text
14
+ class TemplateEngine:
15
+ """
16
+ Renders template strings with variable replacements.
18
17
 
19
- def replace_content(self, path: Path, components: list[str]) -> None:
20
- replacement = generate_replacement_map(components)
18
+ This class uses Python's string.Template to safely substitute
19
+ placeholders in template files with actual values.
20
+ """
21
21
 
22
- with open(path, 'r', encoding='utf-8') as handle:
23
- content = handle.read()
22
+ def render(self, text: str, replacements: dict[str, str]) -> str:
23
+ """
24
+ Renders a template string by replacing placeholders with values.
24
25
 
25
- updated_content = self.apply_replacement(content, replacement)
26
+ :param text: Template string containing ${variable} placeholders.
27
+ :param replacements: Dictionary mapping placeholder names to their values.
28
+ :return: Rendered string with all placeholders replaced.
29
+ """
26
30
 
27
- with open(path, 'w', encoding='utf-8') as handle:
28
- handle.write(updated_content)
31
+ template = Template(text)
32
+ return template.safe_substitute(replacements)
29
33
 
30
- def rename_file(self, path: Path, components: list[str]) -> None:
31
- replacement = generate_replacement_map(components)
32
- new_name = self.apply_replacement(path.name, replacement)
33
34
 
34
- if new_name != path.name:
35
- new_path = path.parent / new_name
36
- path.rename(new_path)
35
+ class TemplateProcessor:
36
+ """
37
+ Processes template files for Django app generation.
38
+
39
+ This class handles the replacement of placeholders in template files
40
+ and manages file renaming based on user configuration.
41
+ """
42
+
43
+ def __init__(self, engine: TemplateEngine, filesystem: FileSystemInterface):
44
+ """
45
+ Initializes the processor with an engine and file system.
46
+
47
+ :param engine: Template engine for rendering strings.
48
+ :param filesystem: File system interface for file operations.
49
+ """
50
+
51
+ self._engine = engine
52
+ self._filesystem = filesystem
37
53
 
38
- def _process_files(
54
+ def process_app_templates(
39
55
  self,
40
56
  directory: Path,
41
57
  components: list[str],
42
- pattern: str,
43
- file_filter: Callable[[Path], bool] | None = None
58
+ user_inputs: dict[str, str] | None = None
44
59
  ) -> None:
45
- for path in directory.rglob(pattern):
46
- if file_filter and not file_filter(path):
47
- continue
48
-
49
- self.replace_content(path, components)
50
- self.rename_file(path, components)
51
-
52
-
53
- class AppTemplateProcessor(BaseTemplateProcessor):
54
- def replace_app_name(self, directory: Path, components: list[str]) -> None:
55
- self._process_files(
56
- directory,
57
- components,
58
- '*.template',
59
- lambda path: path.is_file()
60
- )
61
-
62
- self._process_files(
63
- directory,
64
- components,
65
- '*.py',
66
- lambda path: path.is_file()
67
- )
60
+ """
61
+ Processes all template files in an app directory.
62
+
63
+ Replaces placeholders in .template files, renames them, and updates
64
+ content in .py files based on user inputs.
65
+
66
+ :param directory: Root directory containing template files.
67
+ :param components: List of app path components.
68
+ :param user_inputs: Optional dictionary of user-provided configuration values.
69
+ """
70
+
71
+ for file_path in self._filesystem.iterate_files(directory, '*.template'):
72
+ self._process_file(file_path, components, user_inputs)
73
+
74
+ for file_path in self._filesystem.iterate_files(directory, '*.py'):
75
+ self._replace_content(file_path, components, user_inputs)
76
+ self._rename_file(file_path, components, user_inputs)
68
77
 
69
78
  self._rename_template_files(directory)
70
79
 
80
+ def process_html_templates(
81
+ self,
82
+ directory: Path,
83
+ components: list[str],
84
+ user_inputs: dict[str, str] | None = None
85
+ ) -> None:
86
+ """
87
+ Processes all HTML template files in a directory.
88
+
89
+ Replaces placeholders in .template files and renames them to remove
90
+ the .template extension.
91
+
92
+ :param directory: Root directory containing HTML template files.
93
+ :param components: List of app path components.
94
+ :param user_inputs: Optional dictionary of user-provided configuration values.
95
+ """
96
+
97
+ for file_path in self._filesystem.iterate_files(directory, '*.template'):
98
+ self._process_file(file_path, components, user_inputs)
99
+
100
+ self._rename_template_files(directory)
101
+
102
+ def _process_file(
103
+ self,
104
+ path: Path,
105
+ components: list[str],
106
+ user_inputs: dict[str, str] | None = None
107
+ ) -> None:
108
+ """
109
+ Processes a single template file.
110
+
111
+ Replaces content placeholders and renames the file based on configuration.
112
+
113
+ :param path: Path to the template file.
114
+ :param components: List of app path components.
115
+ :param user_inputs: Optional dictionary of user-provided configuration values.
116
+ """
117
+
118
+ self._replace_content(path, components, user_inputs)
119
+ self._rename_file(path, components, user_inputs)
120
+
121
+ def _rename_file(
122
+ self,
123
+ path: Path,
124
+ components: list[str],
125
+ user_inputs: dict[str, str] | None = None
126
+ ) -> None:
127
+ """
128
+ Renames a file by replacing placeholders in its filename.
129
+
130
+ :param path: Current path of the file.
131
+ :param components: List of app path components.
132
+ :param user_inputs: Optional dictionary of user-provided configuration values.
133
+ """
134
+
135
+ replacement = generate_replacement_map(components, user_inputs)
136
+ new_name = self._engine.render(path.name, replacement)
137
+
138
+ if new_name != path.name:
139
+ new_path = path.parent / new_name
140
+ self._filesystem.rename(path, new_path)
141
+
71
142
  def _rename_template_files(self, directory: Path) -> None:
72
- for template_file in directory.rglob('*.py.template'):
73
- new_name = template_file.name.replace('.py.template', '.py')
74
- new_path = template_file.parent / new_name
75
- template_file.rename(new_path)
143
+ """
144
+ Removes .template extensions from all template files in a directory.
76
145
 
146
+ :param directory: Directory to search for .template files.
147
+ """
77
148
 
78
- class HTMLTemplateProcessor(BaseTemplateProcessor):
79
- def replace_template_names(self, directory: Path, components: list[str]) -> None:
80
- self._process_files(
81
- directory,
82
- components,
83
- '*.html'
84
- )
149
+ for template_file in self._filesystem.iterate_files(directory, '*.template'):
150
+ new_name = template_file.name.replace('.template', '')
151
+ new_path = template_file.parent / new_name
152
+ self._filesystem.rename(template_file, new_path)
153
+
154
+ def _replace_content(
155
+ self,
156
+ path: Path,
157
+ components: list[str],
158
+ user_inputs: dict[str, str] | None = None
159
+ ) -> None:
160
+ """
161
+ Replaces placeholders in a file's content.
162
+
163
+ :param path: Path to the file to process.
164
+ :param components: List of app path components.
165
+ :param user_inputs: Optional dictionary of user-provided configuration values.
166
+ """
167
+
168
+ replacement = generate_replacement_map(components, user_inputs)
169
+ content = self._filesystem.read_file(path)
170
+ updated_content = self._engine.render(content, replacement)
171
+ self._filesystem.write_file(path, updated_content)
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol
4
+
5
+ from django.apps import apps
6
+ from django.conf import settings
7
+
8
+
9
+ class AppRegistryInterface(Protocol):
10
+ """
11
+ Protocol defining the interface for Django app registry operations.
12
+
13
+ This protocol specifies methods for querying registered apps and
14
+ validating app component paths.
15
+ """
16
+
17
+ def get_installed_apps(self) -> list[str]: ...
18
+ def get_missing_components(self, components: list[str]) -> list[str]: ...
19
+ def get_valid_root_apps(self) -> set[str]: ...
20
+ def is_app_registered(self, app_path: str) -> bool: ...
21
+
22
+
23
+ class AppRegistry:
24
+ """
25
+ Manages Django app registration information.
26
+
27
+ This class provides methods to query which apps are installed in the
28
+ Django project and validate app component hierarchies.
29
+ """
30
+
31
+ def get_installed_apps(self) -> list[str]:
32
+ """
33
+ Gets a list of all installed app names.
34
+
35
+ :return: List of fully qualified app names from Django's app registry.
36
+ """
37
+
38
+ return [config.name for config in apps.get_app_configs()]
39
+
40
+ def get_missing_components(self, components: list[str]) -> list[str]:
41
+ """
42
+ Identifies which app components in a path are not registered.
43
+
44
+ For a path like ['app', 'human_resource', 'employee'], this checks
45
+ if 'app', 'app.human_resource', and 'app.human_resource.employee'
46
+ are registered, and returns those that are missing.
47
+
48
+ :param components: List of app path components to check.
49
+ :return: List of unregistered component paths.
50
+ """
51
+
52
+ registry = self.get_installed_apps()
53
+ missing = []
54
+
55
+ total = len(components)
56
+
57
+ for i in range(total):
58
+ component = '.'.join(components[: i + 1])
59
+
60
+ if component not in registry and i > 0:
61
+ missing.append(component)
62
+
63
+ return missing
64
+
65
+ def get_valid_root_apps(self) -> set[str]:
66
+ """
67
+ Gets all valid root app names from INSTALLED_APPS.
68
+
69
+ Returns root-level apps (first component before a dot) that can be
70
+ used as parent apps, excluding Django's built-in apps.
71
+
72
+ :return: Set of valid root app names.
73
+ """
74
+
75
+ return {
76
+ app.split('.')[0]
77
+ for app in settings.INSTALLED_APPS
78
+ if '.' in app and app.split('.')[0] != 'django'
79
+ }
80
+
81
+ def is_app_registered(self, app_path: str) -> bool:
82
+ """
83
+ Checks if an app path is registered in Django.
84
+
85
+ :param app_path: Dot-separated app path to check.
86
+ :return: True if the app is registered, False otherwise.
87
+ """
88
+
89
+ return app_path in self.get_installed_apps()