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.
- django_spire/ai/admin.py +3 -1
- django_spire/ai/apps.py +2 -0
- django_spire/ai/chat/admin.py +15 -9
- django_spire/ai/chat/apps.py +4 -1
- django_spire/ai/chat/auth/controller.py +3 -1
- django_spire/ai/chat/choices.py +2 -0
- django_spire/ai/chat/intelligence/maps/intent_llm_map.py +8 -5
- django_spire/ai/chat/intelligence/prompts.py +4 -2
- django_spire/ai/chat/intelligence/workflows/chat_workflow.py +27 -28
- django_spire/ai/chat/message_intel.py +7 -4
- django_spire/ai/chat/models.py +8 -9
- django_spire/ai/chat/querysets.py +3 -1
- django_spire/ai/chat/responses.py +19 -10
- django_spire/ai/chat/tools.py +20 -15
- django_spire/ai/chat/urls/message_urls.py +2 -1
- django_spire/ai/chat/urls/page_urls.py +1 -0
- django_spire/ai/chat/views/message_request_views.py +2 -0
- django_spire/ai/chat/views/message_response_views.py +4 -4
- django_spire/ai/chat/views/message_views.py +2 -0
- django_spire/ai/chat/views/page_views.py +7 -2
- django_spire/ai/chat/views/template_views.py +2 -0
- django_spire/ai/decorators.py +13 -7
- django_spire/ai/mixins.py +4 -2
- django_spire/ai/models.py +7 -2
- django_spire/ai/prompt/bots.py +14 -32
- django_spire/ai/prompt/intel.py +1 -1
- django_spire/ai/prompt/prompts.py +7 -1
- django_spire/ai/prompt/system/bots.py +42 -75
- django_spire/ai/prompt/system/intel.py +5 -4
- django_spire/ai/prompt/system/prompts.py +5 -1
- django_spire/ai/prompt/system/system_prompt_cli.py +15 -9
- django_spire/ai/prompt/tests/test_bots.py +14 -11
- django_spire/ai/prompt/text_to_prompt_cli.py +5 -2
- django_spire/ai/prompt/tuning/bot_tuning_cli.py +14 -13
- django_spire/ai/prompt/tuning/bots.py +68 -116
- django_spire/ai/prompt/tuning/intel.py +1 -1
- django_spire/ai/prompt/tuning/mixins.py +2 -0
- django_spire/ai/prompt/tuning/prompt_tuning_cli.py +8 -8
- django_spire/ai/prompt/tuning/prompts.py +4 -2
- django_spire/ai/sms/admin.py +3 -1
- django_spire/ai/sms/apps.py +2 -0
- django_spire/ai/sms/decorators.py +2 -0
- django_spire/ai/sms/intel.py +4 -2
- django_spire/ai/sms/intelligence/workflows/sms_conversation_workflow.py +8 -8
- django_spire/ai/sms/models.py +16 -14
- django_spire/ai/sms/querysets.py +4 -1
- django_spire/ai/sms/tools.py +18 -16
- django_spire/ai/sms/urls.py +1 -1
- django_spire/ai/sms/views.py +2 -0
- django_spire/ai/tests/test_ai.py +3 -5
- django_spire/ai/urls.py +1 -0
- django_spire/consts.py +1 -1
- django_spire/contrib/seeding/field/django/seeder.py +6 -4
- django_spire/contrib/seeding/field/llm.py +1 -2
- django_spire/contrib/seeding/intelligence/bots/field_seeding_bots.py +7 -8
- django_spire/contrib/seeding/intelligence/bots/seeder_generator_bot.py +15 -16
- django_spire/contrib/seeding/intelligence/intel.py +1 -1
- django_spire/contrib/seeding/intelligence/prompts/factory.py +5 -7
- django_spire/contrib/seeding/intelligence/prompts/foreign_key_selection_prompt.py +3 -5
- django_spire/contrib/seeding/intelligence/prompts/generate_django_model_seeder_prompts.py +1 -2
- django_spire/contrib/seeding/intelligence/prompts/generic_relationship_selection_prompt.py +3 -5
- django_spire/contrib/seeding/intelligence/prompts/hierarchical_selection_prompt.py +1 -1
- django_spire/contrib/seeding/intelligence/prompts/model_field_choices_prompt.py +3 -3
- django_spire/contrib/seeding/intelligence/prompts/negation_prompt.py +1 -1
- django_spire/contrib/seeding/intelligence/prompts/objective_prompt.py +2 -4
- django_spire/contrib/seeding/management/commands/seeding.py +5 -2
- django_spire/contrib/seeding/model/base.py +12 -10
- django_spire/contrib/seeding/model/django/seeder.py +13 -10
- django_spire/contrib/seeding/tests/test_seeding.py +1 -1
- django_spire/core/management/commands/spire_startapp.py +84 -46
- django_spire/core/management/commands/spire_startapp_pkg/__init__.py +60 -0
- django_spire/core/management/commands/spire_startapp_pkg/builder.py +91 -0
- django_spire/core/management/commands/spire_startapp_pkg/config.py +115 -0
- django_spire/core/management/commands/spire_startapp_pkg/filesystem.py +125 -0
- django_spire/core/management/commands/spire_startapp_pkg/generator.py +167 -0
- django_spire/core/management/commands/spire_startapp_pkg/maps.py +783 -25
- django_spire/core/management/commands/spire_startapp_pkg/permissions.py +147 -0
- django_spire/core/management/commands/spire_startapp_pkg/processor.py +144 -57
- django_spire/core/management/commands/spire_startapp_pkg/registry.py +89 -0
- django_spire/core/management/commands/spire_startapp_pkg/reporter.py +245 -108
- django_spire/core/management/commands/spire_startapp_pkg/resolver.py +86 -0
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +252 -0
- django_spire/core/management/commands/spire_startapp_pkg/validator.py +96 -0
- django_spire/core/middleware/__init__.py +1 -2
- django_spire/knowledge/admin.py +2 -0
- django_spire/knowledge/apps.py +2 -0
- django_spire/knowledge/auth/controller.py +2 -0
- django_spire/knowledge/collection/admin.py +3 -0
- django_spire/knowledge/collection/forms.py +2 -0
- django_spire/knowledge/collection/models.py +4 -0
- django_spire/knowledge/collection/querysets.py +3 -3
- django_spire/knowledge/collection/seeding/seed.py +2 -0
- django_spire/knowledge/collection/tests/factories.py +2 -0
- django_spire/knowledge/collection/tests/test_services/test_transformation_service.py +2 -0
- django_spire/knowledge/collection/tests/test_urls/test_form_urls.py +2 -0
- django_spire/knowledge/collection/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/collection/tests/test_urls/test_page_urls.py +2 -0
- django_spire/knowledge/collection/views/json_views.py +2 -0
- django_spire/knowledge/collection/views/page_views.py +2 -0
- django_spire/knowledge/context_processors.py +2 -0
- django_spire/knowledge/entry/services/tool_service.py +1 -0
- django_spire/knowledge/entry/services/transformation_services.py +0 -1
- django_spire/knowledge/entry/tests/factories.py +3 -0
- django_spire/knowledge/entry/tests/test_urls/test_form_urls.py +2 -0
- django_spire/knowledge/entry/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/entry/tests/test_urls/test_page_urls.py +2 -0
- django_spire/knowledge/entry/urls/form_urls.py +1 -0
- django_spire/knowledge/entry/urls/json_urls.py +1 -0
- django_spire/knowledge/entry/urls/page_urls.py +1 -0
- django_spire/knowledge/entry/urls/template_urls.py +1 -0
- django_spire/knowledge/entry/version/admin.py +2 -0
- django_spire/knowledge/entry/version/block/admin.py +2 -0
- django_spire/knowledge/entry/version/block/blocks/heading_block.py +2 -0
- django_spire/knowledge/entry/version/block/blocks/sub_heading_block.py +2 -0
- django_spire/knowledge/entry/version/block/blocks/text_block.py +2 -0
- django_spire/knowledge/entry/version/block/maps.py +2 -0
- django_spire/knowledge/entry/version/block/models.py +2 -0
- django_spire/knowledge/entry/version/block/tests/factories.py +2 -0
- django_spire/knowledge/entry/version/block/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/entry/version/block/views/json_views.py +2 -0
- django_spire/knowledge/entry/version/intelligence/bots/markdown_format_llm_bot.py +12 -9
- django_spire/knowledge/entry/version/maps.py +2 -0
- django_spire/knowledge/entry/version/models.py +2 -0
- django_spire/knowledge/entry/version/querysets.py +2 -0
- django_spire/knowledge/entry/version/seeding/seeder.py +1 -0
- django_spire/knowledge/entry/version/tests/factories.py +2 -0
- django_spire/knowledge/entry/version/tests/test_converters/test_docx_converter.py +3 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_form_urls.py +2 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_json_urls.py +2 -0
- django_spire/knowledge/entry/version/tests/test_urls/test_page_urls.py +2 -0
- django_spire/knowledge/entry/version/urls/form_urls.py +1 -0
- django_spire/knowledge/entry/version/urls/json_urls.py +1 -0
- django_spire/knowledge/entry/version/urls/page_urls.py +1 -0
- django_spire/knowledge/entry/version/urls/redirect_urls.py +1 -0
- django_spire/knowledge/entry/version/views/form_views.py +2 -0
- django_spire/knowledge/entry/version/views/json_views.py +2 -0
- django_spire/knowledge/entry/version/views/page_views.py +2 -0
- django_spire/knowledge/entry/version/views/redirect_views.py +2 -0
- django_spire/knowledge/exceptions.py +2 -0
- django_spire/knowledge/intelligence/bots/entry_search_llm_bot.py +5 -16
- django_spire/knowledge/intelligence/intel/collection_intel.py +3 -1
- django_spire/knowledge/intelligence/intel/entry_intel.py +3 -3
- django_spire/knowledge/intelligence/intel/message_intel.py +2 -0
- django_spire/knowledge/intelligence/maps/collection_map.py +9 -10
- django_spire/knowledge/intelligence/maps/entry_map.py +8 -9
- django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +8 -6
- django_spire/knowledge/models.py +2 -0
- django_spire/knowledge/seeding/seed.py +2 -0
- django_spire/knowledge/templatetags/spire_knowledge_tags.py +3 -0
- django_spire/knowledge/urls/__init__.py +1 -0
- django_spire/profiling/__init__.py +13 -0
- django_spire/profiling/middleware/__init__.py +6 -0
- django_spire/{core → profiling}/middleware/profiling.py +63 -58
- django_spire/profiling/panel.py +345 -0
- django_spire/profiling/templates/panel.html +166 -0
- {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/METADATA +5 -4
- {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/RECORD +160 -157
- django_spire/core/management/commands/spire_startapp_pkg/constants.py +0 -4
- django_spire/core/management/commands/spire_startapp_pkg/manager.py +0 -176
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_detail_card.html +0 -24
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_form_card.html +0 -9
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/spirechildapp_list_card.html +0 -18
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/spirechildapp_form.html +0 -22
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/spirechildapp_item.html +0 -24
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_detail_page.html +0 -13
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_form_page.html +0 -13
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/spirechildapp_list_page.html +0 -9
- {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/WHEEL +0 -0
- {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.16.12.dist-info → django_spire-0.17.0.dist-info}/top_level.txt +0 -0
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from dandy import BaseIntel, Bot, Prompt
|
|
3
4
|
|
|
4
|
-
class MarkdownFormatLlmBot(BaseLlmBot):
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
class MarkdownTextIntel(BaseIntel):
|
|
7
|
+
text: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MarkdownFormatLlmBot(Bot):
|
|
11
|
+
llm_intel_class = MarkdownTextIntel
|
|
12
|
+
|
|
13
|
+
def process(self, markdown_content: str) -> str:
|
|
11
14
|
markdown_prompt = Prompt()
|
|
12
15
|
markdown_prompt.text(
|
|
13
16
|
'Can you improve the markdown formatting? Do NOT add or change any of the '
|
|
14
17
|
'content.'
|
|
15
18
|
)
|
|
16
|
-
markdown_prompt.line_break()
|
|
17
19
|
markdown_prompt.text(markdown_content)
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
result = self.llm.prompt_to_intel(prompt=markdown_prompt)
|
|
22
|
+
return result.text
|
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dandy
|
|
3
|
+
from dandy import Bot, Prompt
|
|
4
4
|
|
|
5
5
|
from django_spire.knowledge.entry.models import Entry
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class EntrySearchLlmBot(
|
|
8
|
+
class EntrySearchLlmBot(Bot):
|
|
9
9
|
@classmethod
|
|
10
|
-
def process(
|
|
11
|
-
cls,
|
|
12
|
-
user_input: str,
|
|
13
|
-
entry: Entry
|
|
14
|
-
) -> str:
|
|
10
|
+
def process(cls, user_input: str, entry: Entry) -> str:
|
|
15
11
|
entry_prompt = Prompt()
|
|
16
|
-
|
|
17
12
|
entry_prompt.text('You are a helpful assistant that helps users find information about entries.')
|
|
18
|
-
|
|
19
13
|
entry_prompt.text(f'User Input: {user_input}')
|
|
20
|
-
|
|
21
14
|
entry_prompt.text(f'Entry: {entry.name}')
|
|
22
15
|
|
|
23
16
|
for version_block in entry.current_version.blocks.all():
|
|
24
17
|
entry_prompt.text(version_block.render_to_text())
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
prompt=entry_prompt
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
return result_intel.text
|
|
19
|
+
return cls().llm.prompt_to_text(prompt=entry_prompt)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dandy
|
|
3
|
+
from dandy import BaseIntel, BaseListIntel
|
|
4
4
|
|
|
5
5
|
from django_spire.knowledge.intelligence.intel.collection_intel import CollectionIntel
|
|
6
6
|
|
|
@@ -11,4 +11,4 @@ class EntryIntel(BaseIntel):
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class EntriesIntel(BaseListIntel[EntryIntel]):
|
|
14
|
-
entry_intel_list:
|
|
14
|
+
entry_intel_list: list[EntryIntel]
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dandy
|
|
4
|
-
from dandy.map import Map
|
|
3
|
+
from dandy import Decoder
|
|
5
4
|
|
|
6
5
|
from django_spire.knowledge.collection.models import Collection
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
def get_collection_map_class() ->
|
|
10
|
-
class CollectionMap(
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
def get_collection_map_class() -> type[Decoder]:
|
|
9
|
+
class CollectionMap(Decoder):
|
|
10
|
+
mapping_keys_description = 'Knowledge Collection Titles'
|
|
11
|
+
mapping = {
|
|
13
12
|
**{
|
|
14
13
|
collection.name: collection
|
|
15
14
|
for collection in Collection.objects.all().annotate_entry_count()
|
|
16
15
|
},
|
|
17
|
-
|
|
18
|
-
}
|
|
16
|
+
'No Matching Knowledge Collection Titles': None
|
|
17
|
+
}
|
|
19
18
|
|
|
20
|
-
return CollectionMap
|
|
19
|
+
return CollectionMap
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dandy
|
|
4
|
-
from dandy.map import Map
|
|
3
|
+
from dandy import Decoder
|
|
5
4
|
|
|
6
5
|
from django_spire.knowledge.collection.models import Collection
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
def get_entry_map_class(collection: Collection) ->
|
|
10
|
-
class EntryMap(
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
def get_entry_map_class(collection: Collection) -> type[Decoder]:
|
|
9
|
+
class EntryMap(Decoder):
|
|
10
|
+
mapping_keys_description = 'Knowledge Entries'
|
|
11
|
+
mapping = {
|
|
13
12
|
**{
|
|
14
13
|
entry.name: entry
|
|
15
14
|
for entry in collection.entries.all()
|
|
16
15
|
},
|
|
17
|
-
|
|
18
|
-
}
|
|
16
|
+
'No Matching Knowledge Entries': None
|
|
17
|
+
}
|
|
19
18
|
|
|
20
19
|
return EntryMap
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from django_spire.knowledge.intelligence.bots.entry_search_llm_bot import EntrySearchLlmBot
|
|
4
4
|
from django_spire.knowledge.intelligence.intel.collection_intel import CollectionIntel
|
|
5
5
|
from django_spire.knowledge.intelligence.intel.entry_intel import EntriesIntel, EntryIntel
|
|
6
|
-
from django_spire.knowledge.intelligence.maps.collection_map import get_collection_map_class
|
|
7
6
|
from django_spire.knowledge.intelligence.intel.message_intel import KnowledgeMessageIntel
|
|
7
|
+
from django_spire.knowledge.intelligence.maps.collection_map import get_collection_map_class
|
|
8
8
|
from django_spire.knowledge.intelligence.maps.entry_map import get_entry_map_class
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class KnowledgeWorkflow
|
|
11
|
+
class KnowledgeWorkflow:
|
|
12
12
|
@classmethod
|
|
13
13
|
def process(cls, user_input: str) -> KnowledgeMessageIntel:
|
|
14
14
|
CollectionMap = get_collection_map_class()
|
|
15
|
-
collections = CollectionMap.process(user_input)
|
|
15
|
+
collections = CollectionMap().process(user_input).values
|
|
16
16
|
|
|
17
17
|
if collections[0] is None:
|
|
18
18
|
return KnowledgeMessageIntel(
|
|
@@ -26,7 +26,7 @@ class KnowledgeWorkflow(BaseWorkflow):
|
|
|
26
26
|
for collection in collections:
|
|
27
27
|
if collection.entry_count > 0:
|
|
28
28
|
EntryMap = get_entry_map_class(collection=collection)
|
|
29
|
-
entries.extend(EntryMap.process(user_input))
|
|
29
|
+
entries.extend(EntryMap().process(user_input).values)
|
|
30
30
|
|
|
31
31
|
entries = [entry for entry in entries if entry is not None]
|
|
32
32
|
|
|
@@ -38,10 +38,12 @@ class KnowledgeWorkflow(BaseWorkflow):
|
|
|
38
38
|
)
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
entry_search_bot = EntrySearchLlmBot()
|
|
42
|
+
|
|
41
43
|
entries_intel = EntriesIntel(
|
|
42
44
|
entry_intel_list=[
|
|
43
45
|
EntryIntel(
|
|
44
|
-
body=
|
|
46
|
+
body=entry_search_bot.process(
|
|
45
47
|
user_input=user_input,
|
|
46
48
|
entry=entry
|
|
47
49
|
),
|
django_spire/knowledge/models.py
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
lock = threading.Lock()
|
|
4
|
+
|
|
5
|
+
from django_spire.profiling.middleware.profiling import ProfilingMiddleware
|
|
6
|
+
from django_spire.profiling.panel import ProfilingPanel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'ProfilingMiddleware',
|
|
11
|
+
'ProfilingPanel',
|
|
12
|
+
'lock'
|
|
13
|
+
]
|
|
@@ -5,20 +5,42 @@ import threading
|
|
|
5
5
|
import time
|
|
6
6
|
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing_extensions import
|
|
8
|
+
from typing_extensions import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
from django.conf import settings
|
|
11
11
|
from django.utils.deprecation import MiddlewareMixin
|
|
12
12
|
|
|
13
|
+
from django_spire.profiling import lock
|
|
14
|
+
|
|
13
15
|
try:
|
|
14
16
|
from pyinstrument import Profiler
|
|
15
17
|
except ImportError:
|
|
16
18
|
Profiler = None
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
21
|
+
from typing_extensions import Any, Callable
|
|
22
|
+
|
|
19
23
|
from django.http import HttpRequest, HttpResponse
|
|
20
24
|
|
|
21
25
|
|
|
26
|
+
IGNORE_EXTENSION = [
|
|
27
|
+
'.eot',
|
|
28
|
+
'.gif',
|
|
29
|
+
'.ico',
|
|
30
|
+
'.jpeg',
|
|
31
|
+
'.jpg',
|
|
32
|
+
'.js',
|
|
33
|
+
'.map',
|
|
34
|
+
'.pdf',
|
|
35
|
+
'.png',
|
|
36
|
+
'.svg',
|
|
37
|
+
'.ttf',
|
|
38
|
+
'.txt',
|
|
39
|
+
'.woff',
|
|
40
|
+
'.woff2',
|
|
41
|
+
'.zip',
|
|
42
|
+
]
|
|
43
|
+
|
|
22
44
|
IGNORE_PATH = [
|
|
23
45
|
'/__',
|
|
24
46
|
'/__debug__/',
|
|
@@ -36,12 +58,13 @@ IGNORE_PATH = [
|
|
|
36
58
|
'/debug/',
|
|
37
59
|
'/debug-toolbar/',
|
|
38
60
|
'/django_glue/',
|
|
61
|
+
'/django_spire/theme/json/get_config/',
|
|
39
62
|
'/docs/',
|
|
40
63
|
'/favicon.ico',
|
|
41
64
|
'/media/',
|
|
42
65
|
'/openapi/',
|
|
43
66
|
'/redoc/',
|
|
44
|
-
'/robots.txt',
|
|
67
|
+
'/robots.txt/',
|
|
45
68
|
'/schema/',
|
|
46
69
|
'/sitemap.xml',
|
|
47
70
|
'/static/',
|
|
@@ -50,37 +73,15 @@ IGNORE_PATH = [
|
|
|
50
73
|
]
|
|
51
74
|
|
|
52
75
|
|
|
53
|
-
IGNORE_EXTENSION = [
|
|
54
|
-
'.css',
|
|
55
|
-
'.eot',
|
|
56
|
-
'.gif',
|
|
57
|
-
'.ico',
|
|
58
|
-
'.jpeg',
|
|
59
|
-
'.jpg',
|
|
60
|
-
'.js',
|
|
61
|
-
'.map',
|
|
62
|
-
'.pdf',
|
|
63
|
-
'.png',
|
|
64
|
-
'.svg',
|
|
65
|
-
'.ttf',
|
|
66
|
-
'.txt',
|
|
67
|
-
'.woff',
|
|
68
|
-
'.woff2',
|
|
69
|
-
'.zip',
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
|
|
73
76
|
class ProfilingMiddleware(MiddlewareMixin):
|
|
74
77
|
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
75
78
|
super().__init__(get_response)
|
|
76
79
|
|
|
77
|
-
if Profiler is None:
|
|
78
|
-
message = 'pyinstrument is required for profiling.'
|
|
79
|
-
raise ImportError(message)
|
|
80
|
-
|
|
81
80
|
configuration = {
|
|
82
81
|
'PROFILING_DIR': os.getenv('PROFILING_DIR', '.profile'),
|
|
83
82
|
'PROFILING_ENABLED': os.getenv('PROFILING_ENABLED', 'False') == 'True',
|
|
83
|
+
'PROFILING_MAX_FILES': int(os.getenv('PROFILING_MAX_FILES', '10')),
|
|
84
|
+
'PROFILE_THRESHOLD': float(os.getenv('PROFILE_THRESHOLD', '0')),
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
directory = configuration.get('PROFILING_DIR', '.profile')
|
|
@@ -97,32 +98,28 @@ class ProfilingMiddleware(MiddlewareMixin):
|
|
|
97
98
|
self.directory.mkdir(exist_ok=True)
|
|
98
99
|
|
|
99
100
|
self.enabled = configuration.get('PROFILING_ENABLED', False)
|
|
100
|
-
self.
|
|
101
|
+
self.threshold = configuration.get('PROFILE_THRESHOLD', 0)
|
|
102
|
+
self.maximum = configuration.get('PROFILING_MAX_FILES', 10)
|
|
101
103
|
|
|
102
104
|
self.count = 0
|
|
103
105
|
self.lock = threading.Lock()
|
|
104
106
|
|
|
105
|
-
def
|
|
106
|
-
files = self.directory.glob('*.html')
|
|
107
|
-
|
|
107
|
+
def _remove_profiles(self) -> None:
|
|
108
|
+
files = list(self.directory.glob('*.html'))
|
|
109
|
+
|
|
110
|
+
if len(files) <= self.maximum:
|
|
111
|
+
return
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
key=lambda p: p.stat().st_mtime,
|
|
113
|
+
files.sort(
|
|
114
|
+
key=lambda p: p.stat().st_mtime if p.exists() else 0,
|
|
111
115
|
reverse=True
|
|
112
116
|
)
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if len(profiles) > maximum:
|
|
117
|
-
for profile in profiles[maximum:]:
|
|
118
|
+
for profile in files[self.maximum:]:
|
|
119
|
+
if profile.exists():
|
|
118
120
|
profile.unlink()
|
|
119
121
|
|
|
120
|
-
def _save_profile(
|
|
121
|
-
self,
|
|
122
|
-
profiler: Profiler,
|
|
123
|
-
request: HttpRequest,
|
|
124
|
-
duration_ms: float
|
|
125
|
-
) -> None:
|
|
122
|
+
def _save_profile(self, profiler: Profiler, request: HttpRequest, duration: float) -> None:
|
|
126
123
|
with self.lock:
|
|
127
124
|
timestamp = int(time.time() * 1000)
|
|
128
125
|
method = request.method
|
|
@@ -131,21 +128,29 @@ class ProfilingMiddleware(MiddlewareMixin):
|
|
|
131
128
|
if not path or path == '_':
|
|
132
129
|
path = 'root'
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
profiler.write_html(path)
|
|
131
|
+
profileid = request._profiling_id
|
|
132
|
+
filename = f'{timestamp}_{method}_{path}_{duration:.1f}ms_{profileid}.html'
|
|
133
|
+
filepath = self.directory / filename
|
|
138
134
|
|
|
139
|
-
|
|
135
|
+
with lock:
|
|
136
|
+
profiler.write_html(str(filepath))
|
|
137
|
+
self._remove_profiles()
|
|
140
138
|
|
|
141
|
-
def
|
|
139
|
+
def _should_skip(self, request: HttpRequest) -> bool:
|
|
142
140
|
path = request.path
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
path_match = any(
|
|
143
|
+
path.startswith(pattern) or pattern in path
|
|
144
|
+
for pattern in IGNORE_PATH
|
|
147
145
|
)
|
|
148
146
|
|
|
147
|
+
extension_match = any(
|
|
148
|
+
path.endswith(extension) or extension in path
|
|
149
|
+
for extension in IGNORE_EXTENSION
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return path_match or extension_match
|
|
153
|
+
|
|
149
154
|
def process_view(
|
|
150
155
|
self,
|
|
151
156
|
request: HttpRequest,
|
|
@@ -159,7 +164,7 @@ class ProfilingMiddleware(MiddlewareMixin):
|
|
|
159
164
|
if not self.enabled:
|
|
160
165
|
return None
|
|
161
166
|
|
|
162
|
-
if self.
|
|
167
|
+
if self._should_skip(request):
|
|
163
168
|
return None
|
|
164
169
|
|
|
165
170
|
with self.lock:
|
|
@@ -167,7 +172,7 @@ class ProfilingMiddleware(MiddlewareMixin):
|
|
|
167
172
|
request._profiling_id = self.count
|
|
168
173
|
|
|
169
174
|
profiler = Profiler(interval=0.001)
|
|
170
|
-
|
|
175
|
+
start = time.time()
|
|
171
176
|
profiler.start()
|
|
172
177
|
|
|
173
178
|
try:
|
|
@@ -177,15 +182,15 @@ class ProfilingMiddleware(MiddlewareMixin):
|
|
|
177
182
|
response.render()
|
|
178
183
|
except Exception:
|
|
179
184
|
profiler.stop()
|
|
180
|
-
|
|
181
|
-
self._save_profile(profiler, request,
|
|
185
|
+
duration = (time.time() - start) * 1000
|
|
186
|
+
self._save_profile(profiler, request, duration)
|
|
182
187
|
|
|
183
188
|
raise
|
|
184
189
|
else:
|
|
185
190
|
profiler.stop()
|
|
186
|
-
|
|
191
|
+
duration = (time.time() - start) * 1000
|
|
187
192
|
|
|
188
|
-
if
|
|
189
|
-
self._save_profile(profiler, request,
|
|
193
|
+
if duration >= self.threshold:
|
|
194
|
+
self._save_profile(profiler, request, duration)
|
|
190
195
|
|
|
191
196
|
return response
|