stark-engine 4.3.1__tar.gz → 4.4.1__tar.gz

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 (76) hide show
  1. {stark_engine-4.3.1 → stark_engine-4.4.1}/PKG-INFO +3 -2
  2. {stark_engine-4.3.1 → stark_engine-4.4.1}/pyproject.toml +4 -2
  3. stark_engine-4.4.1/stark/__init__.py +79 -0
  4. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/command.py +16 -23
  5. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/commands_context.py +16 -3
  6. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/commands_context_processor.py +6 -2
  7. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/commands_manager.py +8 -3
  8. stark_engine-4.4.1/stark/core/health_check.py +74 -0
  9. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/parsing.py +198 -26
  10. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/patterns/pattern.py +0 -1
  11. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/processors/__init__.py +1 -0
  12. stark_engine-4.4.1/stark/core/processors/corrections_processor.py +81 -0
  13. stark_engine-4.4.1/stark/core/processors/search_processor.py +176 -0
  14. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/processors/spacy_ner_processor.py +16 -5
  15. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/types/object.py +7 -2
  16. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/types/slots.py +2 -1
  17. stark_engine-4.4.1/stark/general/feature_flags.py +17 -0
  18. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/general/json_encoder.py +1 -1
  19. stark_engine-4.4.1/stark/general/localisation/__init__.py +6 -0
  20. stark_engine-4.4.1/stark/general/localisation/language_code.py +13 -0
  21. stark_engine-4.4.1/stark/general/localisation/locale_string.py +188 -0
  22. stark_engine-4.4.1/stark/general/localisation/localizable_string.py +40 -0
  23. stark_engine-4.4.1/stark/general/localisation/localizer.py +154 -0
  24. stark_engine-4.4.1/stark/general/localisation/strings.py +47 -0
  25. stark_engine-4.4.1/stark/interfaces/microphone.py +61 -0
  26. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/interfaces/protocols.py +5 -2
  27. stark_engine-4.4.1/stark/interfaces/recognizer_relay.py +207 -0
  28. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/interfaces/vosk.py +99 -57
  29. stark_engine-4.4.1/stark/models/__init__.py +0 -0
  30. stark_engine-4.4.1/stark/models/transcription_string.py +201 -0
  31. stark_engine-4.4.1/stark/models/voice_transcription.py +291 -0
  32. stark_engine-4.4.1/stark/models/voice_transcription_string.py +133 -0
  33. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/__init__.py +1 -0
  34. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/dictionary.py +9 -8
  35. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/models.py +4 -3
  36. stark_engine-4.4.1/stark/tools/dictionary/recognizable_dictionary.py +28 -0
  37. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/storage/storage_memory.py +4 -2
  38. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/storage/storage_sqlite.py +4 -2
  39. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/__init__.py +7 -3
  40. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/epitran.py +5 -3
  41. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/espeak.py +3 -1
  42. stark_engine-4.4.1/stark/tools/phonetic/transcription/latin_passthrough.py +25 -0
  43. stark_engine-4.4.1/stark/tools/phonetic/transcription/protocol.py +7 -0
  44. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/voice_assistant/voice_assistant.py +21 -10
  45. stark_engine-4.3.1/stark/__init__.py +0 -50
  46. stark_engine-4.3.1/stark/core/health_check.py +0 -51
  47. stark_engine-4.3.1/stark/core/processors/search_processor.py +0 -82
  48. stark_engine-4.3.1/stark/tools/phonetic/transcription/protocol.py +0 -5
  49. {stark_engine-4.3.1 → stark_engine-4.4.1}/LICENSE.md +0 -0
  50. {stark_engine-4.3.1 → stark_engine-4.4.1}/README.md +0 -0
  51. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/__init__.py +0 -0
  52. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/patterns/__init__.py +0 -0
  53. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/patterns/rules.py +0 -0
  54. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/types/__init__.py +0 -0
  55. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/types/location.py +0 -0
  56. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/types/string.py +0 -0
  57. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/core/types/word.py +0 -0
  58. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/general/blockage_detector.py +0 -0
  59. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/general/cache.py +0 -0
  60. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/general/classproperty.py +0 -0
  61. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/general/dependencies.py +0 -0
  62. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/interfaces/gcloud.py +0 -0
  63. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/interfaces/silero.py +0 -0
  64. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/common/span.py +0 -0
  65. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/!examples.py +0 -0
  66. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/nl_dictionary_name.py +0 -0
  67. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/dictionary/storage/__init__.py +0 -0
  68. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/levenshtein/__init__.py +0 -0
  69. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/levenshtein/levenshtein.pyi +0 -0
  70. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/levenshtein/levenshtein.pyx +0 -0
  71. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/phonetic/simplephone.py +0 -0
  72. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/ipa2lat.py +0 -0
  73. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/sliding_window_parser.py +0 -0
  74. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/tools/strtools.py +0 -0
  75. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/voice_assistant/__init__.py +0 -0
  76. {stark_engine-4.3.1 → stark_engine-4.4.1}/stark/voice_assistant/mode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stark-engine
3
- Version: 4.3.1
3
+ Version: 4.4.1
4
4
  Summary: S.T.A.R.K - Speech and Text Algorithmic Recognition Kit. Modern framework for creating powerfull voice assistants.
5
5
  License: CC BY-NC-SA 4.0
6
6
  License-File: LICENSE.md
@@ -19,7 +19,8 @@ Provides-Extra: silero
19
19
  Provides-Extra: sound
20
20
  Provides-Extra: spacy
21
21
  Provides-Extra: vosk
22
- Requires-Dist: asyncer (>=0.0.2,<0.0.3)
22
+ Requires-Dist: anyio (>=4.0.0,<5.0.0)
23
+ Requires-Dist: asyncer (>=0.0.8,<1.0.0)
23
24
  Requires-Dist: google-cloud-texttospeech (>=2.14.1,<3.0.0) ; extra == "gcloud" or extra == "all"
24
25
  Requires-Dist: numpy (>=2.3.3,<3.0.0) ; extra == "silero" or extra == "all"
25
26
  Requires-Dist: pydantic (>=2.0.0,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "stark-engine"
3
- version = "4.3.1"
3
+ version = "4.4.1"
4
4
  description = "S.T.A.R.K - Speech and Text Algorithmic Recognition Kit. Modern framework for creating powerfull voice assistants."
5
5
  authors = ["MarkParker5 <mark@parker-programs.com>"]
6
6
  license = "CC BY-NC-SA 4.0"
@@ -39,7 +39,8 @@ keywords = [
39
39
  [tool.poetry.dependencies]
40
40
  python = ">=3.12,<3.15"
41
41
  pydantic = "^2.0.0"
42
- asyncer = "^0.0.2"
42
+ asyncer = ">=0.0.8,<1.0.0"
43
+ anyio = ">=4.0.0,<5.0.0"
43
44
  numpy = "^2.3.3"
44
45
 
45
46
  # Optional dependencies
@@ -85,6 +86,7 @@ coverage = "^7.11.0"
85
86
  faker = "^37.11.0"
86
87
  pympler = "^1.1"
87
88
  psutil = "^7.1.1"
89
+ trio = "^0.33.0"
88
90
 
89
91
  [build-system]
90
92
  requires = ["poetry-core"]
@@ -0,0 +1,79 @@
1
+ import asyncer
2
+
3
+ from stark.core import (
4
+ Command,
5
+ CommandsContext,
6
+ CommandsManager,
7
+ Pattern,
8
+ Response,
9
+ ResponseStatus,
10
+ )
11
+ from stark.core.commands_context_processor import CommandsContextProcessor
12
+ from stark.core.health_check import health_check
13
+ from stark.core.processors.search_processor import SearchProcessor
14
+ from stark.general.blockage_detector import BlockageDetector
15
+ from stark.general.localisation import Localizer
16
+ from stark.interfaces.protocols import (
17
+ SpeechRecognizer,
18
+ SpeechRecognizerDelegate,
19
+ SpeechSynthesizer,
20
+ SpeechSynthesizerResult,
21
+ )
22
+ from stark.voice_assistant import Mode, VoiceAssistant
23
+
24
+
25
+ async def run(
26
+ manager: CommandsManager,
27
+ speech_recognizer: SpeechRecognizer | list[SpeechRecognizer],
28
+ speech_synthesizer: SpeechSynthesizer,
29
+ processors: list[CommandsContextProcessor] | None = None,
30
+ localizer: Localizer | None = None,
31
+ ):
32
+ if processors is None:
33
+ if localizer:
34
+ from stark.core.processors.corrections_processor import CorrectionsProcessor
35
+ processors = [CorrectionsProcessor(), SearchProcessor()]
36
+ else:
37
+ processors = [SearchProcessor()]
38
+
39
+ async with asyncer.create_task_group() as main_task_group:
40
+ context = CommandsContext(
41
+ task_group=main_task_group,
42
+ commands_manager=manager,
43
+ processors=processors,
44
+ localizer=localizer,
45
+ )
46
+
47
+ from stark.interfaces.microphone import Microphone
48
+
49
+ recognizers = speech_recognizer if isinstance(speech_recognizer, list) else [speech_recognizer]
50
+ use_relay = len(recognizers) > 1
51
+
52
+ if use_relay:
53
+ from stark.interfaces.recognizer_relay import SpeechRecognizerRelay
54
+ effective_recognizer = SpeechRecognizerRelay(recognizers)
55
+ else:
56
+ effective_recognizer = recognizers[0]
57
+
58
+ voice_assistant = VoiceAssistant(
59
+ speech_recognizer=effective_recognizer,
60
+ speech_synthesizer=speech_synthesizer,
61
+ commands_context=context,
62
+ )
63
+ effective_recognizer.delegate = voice_assistant
64
+ context.delegate = voice_assistant
65
+
66
+ health_check(context.pattern_parser, manager.commands)
67
+
68
+ if use_relay:
69
+ effective_recognizer.start_speech_recognizers(main_task_group)
70
+ else:
71
+ main_task_group.soonify(effective_recognizer.start_listening)()
72
+
73
+ microphone = Microphone(effective_recognizer.microphone_did_receive_sample)
74
+ main_task_group.soonify(microphone.start_listening)()
75
+
76
+ main_task_group.soonify(context.handle_responses)()
77
+
78
+ detector = BlockageDetector()
79
+ main_task_group.soonify(detector.monitor)()
@@ -29,12 +29,11 @@ import asyncer
29
29
  from pydantic import BaseModel, Field
30
30
 
31
31
  from ..general.classproperty import classproperty
32
+ from ..general.localisation import LanguageCode, LocalizableString
32
33
  from .patterns import Pattern
33
34
 
34
35
  ResponseOptions = (
35
- Optional["Response"]
36
- | Generator[Optional["Response"], None, None]
37
- | AsyncGenerator[Optional["Response"], None]
36
+ Optional["Response"] | Generator[Optional["Response"], None, None] | AsyncGenerator[Optional["Response"], None]
38
37
  )
39
38
  AwaitResponse = Awaitable[ResponseOptions]
40
39
  AsyncCommandRunner = Callable[..., AwaitResponse]
@@ -44,16 +43,21 @@ CommandRunner = TypeVar("CommandRunner", bound=SyncCommandRunner | AsyncCommandR
44
43
 
45
44
  class Command(Generic[CommandRunner]):
46
45
  name: str
47
- pattern: Pattern
46
+ patterns: dict[LanguageCode, Pattern]
48
47
  _runner: CommandRunner
49
48
 
50
- def __init__(self, name: str, pattern: Pattern, runner: CommandRunner):
51
- assert isinstance(pattern, Pattern)
49
+ def __init__(self, name: str, patterns: dict[LanguageCode, Pattern], runner: CommandRunner):
50
+ assert patterns and all(isinstance(p, Pattern) for p in patterns.values())
52
51
  self.name = name
53
- self.pattern = pattern
52
+ self.patterns = patterns
54
53
  self._runner = runner
55
54
  update_wrapper(self, runner)
56
55
 
56
+ def get_pattern(self, language_code: LanguageCode) -> Pattern:
57
+ if language_code in self.patterns:
58
+ return self.patterns[language_code]
59
+ return self.patterns["base"]
60
+
57
61
  def run(
58
62
  self,
59
63
  parameters_dict: dict[str, Any] | None = None,
@@ -74,9 +78,7 @@ class Command(Generic[CommandRunner]):
74
78
 
75
79
  runner: AsyncCommandRunner
76
80
 
77
- if inspect.iscoroutinefunction(self._runner) or inspect.isasyncgen(
78
- self._runner
79
- ):
81
+ if inspect.iscoroutinefunction(self._runner) or inspect.isasyncgen(self._runner):
80
82
  # async functions (coroutines) and async generators are remain as is
81
83
  runner = cast(AsyncCommandRunner, self._runner)
82
84
  else:
@@ -84,21 +86,12 @@ class Command(Generic[CommandRunner]):
84
86
  # async generators are not supported yet by asyncer.asyncify (https://github.com/tiangolo/asyncer/discussions/86)
85
87
  runner = asyncer.asyncify(cast(SyncCommandRunner, self._runner))
86
88
 
87
- if any(
88
- p.kind == p.VAR_KEYWORD
89
- for p in inspect.signature(self._runner).parameters.values()
90
- ):
89
+ if any(p.kind == p.VAR_KEYWORD for p in inspect.signature(self._runner).parameters.values()):
91
90
  # if command runner accepts **kwargs, pass all parameters
92
91
  coroutine = runner(**parameters)
93
92
  else:
94
93
  # otherwise pass only parameters that are in command runner signature to prevent TypeError: got an unexpected keyword argument
95
- coroutine = runner(
96
- **{
97
- k: v
98
- for k, v in parameters.items()
99
- if k in self._runner.__code__.co_varnames
100
- }
101
- )
94
+ coroutine = runner(**{k: v for k, v in parameters.items() if k in self._runner.__code__.co_varnames})
102
95
 
103
96
  @wraps(runner)
104
97
  async def coroutine_wrapper() -> ResponseOptions:
@@ -139,8 +132,8 @@ class ResponseStatus(Enum):
139
132
 
140
133
 
141
134
  class Response(BaseModel):
142
- voice: str = ""
143
- text: str = ""
135
+ voice: str | LocalizableString = ""
136
+ text: str | LocalizableString = ""
144
137
  status: ResponseStatus = ResponseStatus.success
145
138
  needs_user_input: bool = False
146
139
  commands: list[Command] = []
@@ -15,8 +15,10 @@ from stark.core.commands_context_processor import (
15
15
  )
16
16
  from stark.core.parsing import PatternParser
17
17
  from stark.core.types.object import Object
18
+ from stark.general.localisation import LocaleString, Localizer
19
+ from stark.general.localisation.language_code import LanguageCode
18
20
 
19
- from ..general.dependencies import DependencyManager, default_dependency_manager
21
+ from ..general.dependencies import Dependency, DependencyManager, default_dependency_manager
20
22
  from .command import (
21
23
  AsyncResponseHandler,
22
24
  Command,
@@ -57,11 +59,12 @@ class CommandsContext:
57
59
  commands_manager: CommandsManager,
58
60
  dependency_manager: DependencyManager = default_dependency_manager,
59
61
  processors: list[Any] | None = None,
62
+ localizer: Localizer | None = None,
60
63
  ):
61
64
  assert isinstance(task_group, TaskGroup), task_group
62
65
  assert isinstance(commands_manager, CommandsManager)
63
66
  assert isinstance(dependency_manager, DependencyManager)
64
- self.pattern_parser = PatternParser()
67
+ self.pattern_parser = PatternParser(localizer=localizer)
65
68
  self.commands_manager = commands_manager
66
69
  self.context_queue = [self.root_context]
67
70
  self._response_queue = []
@@ -88,7 +91,9 @@ class CommandsContext:
88
91
  def root_context(self):
89
92
  return CommandsContextLayer(self.commands_manager.commands, {})
90
93
 
91
- async def process_string(self, string: str):
94
+ async def process_string(self, string: str | LocaleString):
95
+ string = LocaleString(string) if not isinstance(string, LocaleString) else string
96
+
92
97
  if not self.context_queue:
93
98
  self.context_queue.append(self.root_context)
94
99
 
@@ -117,7 +122,15 @@ class CommandsContext:
117
122
  parameters: dict[str, Object] = {}
118
123
  parameters.update(current_context.parameters)
119
124
  parameters.update(search_result.match_result.parameters)
125
+
126
+ substring = search_result.match_result.substring
127
+ lang = substring.language_code if isinstance(substring, LocaleString) else string.language_code
128
+ lang_dep = Dependency(None, LanguageCode, lang)
129
+ self.dependency_manager.dependencies.add(lang_dep)
120
130
  parameters.update(self.dependency_manager.resolve(search_result.command._runner))
131
+ # language is a command-specific temporary dependency
132
+ self.dependency_manager.dependencies.discard(lang_dep)
133
+
121
134
  self.run_command(search_result.command, parameters)
122
135
 
123
136
  return search_results or []
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
8
8
  from stark.core.command import Command
9
9
  from stark.core.parsing import RecognizedEntity
10
10
  from stark.core.types.object import Object
11
+ from stark.general.localisation import LocaleString
11
12
 
12
13
  from .commands_manager import SearchResult
13
14
 
@@ -42,7 +43,10 @@ class CommandsContextProcessor(ABC):
42
43
  """
43
44
 
44
45
  async def process_string(
45
- self, string: str, context: CommandsContext, recognized_entities: list[RecognizedEntity]
46
+ self,
47
+ string: LocaleString,
48
+ context: CommandsContext,
49
+ recognized_entities: list[RecognizedEntity],
46
50
  ) -> tuple[list[SearchResult], int]:
47
51
  """
48
52
  Processes the entire context queue.
@@ -72,7 +76,7 @@ class CommandsContextProcessor(ABC):
72
76
 
73
77
  async def process_context_layer(
74
78
  self,
75
- string: str,
79
+ string: LocaleString,
76
80
  context: CommandsContext,
77
81
  context_layer: CommandsContextLayer,
78
82
  recognized_entities: list[RecognizedEntity],
@@ -31,9 +31,14 @@ class CommandsManager:
31
31
  return command
32
32
  return None
33
33
 
34
- def new(self, pattern_str: str, hidden: bool = False):
34
+ def new(self, pattern_str: str | dict[str, str], hidden: bool = False):
35
35
  def creator(runner: CommandRunner) -> Command:
36
- pattern = Pattern(pattern_str)
36
+ if isinstance(pattern_str, dict):
37
+ patterns = {lang: Pattern(p) for lang, p in pattern_str.items()}
38
+ if 'base' not in patterns:
39
+ patterns['base'] = next(iter(patterns.values()))
40
+ else:
41
+ patterns = {'base': Pattern(pattern_str)}
37
42
 
38
43
  # take the main type from Optionals
39
44
  annotations = dict()
@@ -60,7 +65,7 @@ class CommandsManager:
60
65
 
61
66
  # create command
62
67
 
63
- cmd = Command(f"{self.name}.{runner.__name__}", pattern, runner)
68
+ cmd = Command(f"{self.name}.{runner.__name__}", patterns, runner)
64
69
 
65
70
  if not hidden:
66
71
  self.commands.append(cmd)
@@ -0,0 +1,74 @@
1
+ import warnings
2
+
3
+ from stark.core.command import Command
4
+ from stark.core.parsing import _LOCALIZER_KEY_REGEX, PatternParser
5
+
6
+
7
+ def health_check(pattern_parser: PatternParser, commands: list[Command]) -> None:
8
+ # TODO: update schema and validation; handle optional parameters; handle short form where type is defined in object
9
+
10
+ # 1. Registered types: pattern parameters and annotations match; all used types are registered
11
+ for reg_type in pattern_parser.parameter_types_by_name.values():
12
+ for lang, type_pattern in reg_type.type.patterns.items():
13
+ pattern_params = set(type_pattern.parameters.keys())
14
+ class_params = set(reg_type.type.__annotations__.keys())
15
+ assert pattern_params <= class_params, (
16
+ f"Pattern parameters {pattern_params} do not match class annotations {class_params} for {reg_type.type.__name__} (language '{lang}')"
17
+ )
18
+ # 2. Registered types: All referenced parameter types are registered
19
+ for param_name in pattern_params:
20
+ assert type_pattern.parameters[param_name].type_name in pattern_parser.parameter_types_by_name, (
21
+ f"Unknown parameter type '{param_name}' in pattern '{type_pattern}' of type '{reg_type.type.__name__}' (language '{lang}'). Known types: {sorted(pattern_parser.parameter_types_by_name.keys())}"
22
+ )
23
+
24
+ # 3. No duplicate parameter types registered
25
+ type_names = list(pattern_parser.parameter_types_by_name.keys())
26
+ if len(type_names) != len(set(type_names)):
27
+ duplicates = {name for name in type_names if type_names.count(name) > 1}
28
+ assert not duplicates, f"Duplicate parameter type names found: {sorted(duplicates)}"
29
+
30
+ # 4. Commands: validate @key references and parameter types
31
+ for command in commands:
32
+ for lang, cmd_pattern in command.patterns.items():
33
+ if pattern_parser.localizer and pattern_parser._has_localizer_keys(cmd_pattern):
34
+ for key in _LOCALIZER_KEY_REGEX.findall(cmd_pattern._origin):
35
+ pattern_parser.localizer.verify_recognizable(key)
36
+ for param in command.get_pattern("base").parameters.values():
37
+ assert param.type_name in pattern_parser.parameter_types_by_name, (
38
+ f"Unknown parameter type '{param.type_name}' in pattern '{command.get_pattern('base')}' of command '{command.name}'. Known types: {sorted(pattern_parser.parameter_types_by_name.keys())}"
39
+ )
40
+
41
+ # 5. Commands: all pattern params are expected by the runner func
42
+ for command in commands:
43
+ pattern_params = set(command.get_pattern("base").parameters.keys())
44
+ runner_params = set(command._runner.__annotations__.keys())
45
+ assert pattern_params <= runner_params, (
46
+ f"Command '{command.name}' function missing parameters: {pattern_params - runner_params}"
47
+ )
48
+
49
+ # 6. Pattern syntax validity (TODO)
50
+ # for command in commands:
51
+ # assert command.get_pattern("base").is_valid(), f"Invalid pattern syntax: {command.get_pattern("base").origin}"
52
+
53
+ # 7. Try compiling all patterns to catch syntax errors early
54
+ for reg_type in pattern_parser.parameter_types_by_name.values():
55
+ for lang, p in reg_type.type.patterns.items():
56
+ try:
57
+ pattern_parser._compile_pattern(p, language_code=lang)
58
+ except Exception as e:
59
+ warnings.warn(
60
+ f"Failed to compile pattern '{p}' of type '{reg_type.type.__name__}' (language '{lang}'): {e}"
61
+ )
62
+ for command in commands:
63
+ for lang, p in command.patterns.items():
64
+ try:
65
+ pattern_parser._compile_pattern(p, language_code=lang)
66
+ except Exception as e:
67
+ warnings.warn(f"Failed to compile pattern '{p}' of command '{command.name}' (language '{lang}'): {e}")
68
+
69
+ # 8. No unused types
70
+ used_types = {param.type_name for command in commands for param in command.get_pattern("base").parameters.values()}
71
+ for type_name in pattern_parser.parameter_types_by_name:
72
+ # assert type_name in used_types, f"Registered type {type_name} is not used in any pattern"
73
+ if type_name not in used_types:
74
+ warnings.warn(f"Registered type {type_name} is not used in any pattern")