soliplex 0.78.2__tar.gz → 0.79__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 (97) hide show
  1. {soliplex-0.78.2/src/soliplex.egg-info → soliplex-0.79}/PKG-INFO +3 -3
  2. {soliplex-0.78.2 → soliplex-0.79}/pyproject.toml +11 -6
  3. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/agui/persistence.py +0 -3
  4. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/agui/util.py +1 -1
  5. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/authz/__init__.py +9 -7
  6. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/authz/schema.py +1 -1
  7. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/audit.py +45 -6
  8. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/agents.py +8 -5
  9. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/authsystem.py +5 -2
  10. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/completions.py +6 -2
  11. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/installation.py +35 -36
  12. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/logfire.py +5 -2
  13. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/quizzes.py +6 -2
  14. soliplex-0.79/src/soliplex/config/rag.py +508 -0
  15. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/rooms.py +25 -27
  16. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/secrets.py +8 -5
  17. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/skills.py +13 -16
  18. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/tools.py +25 -17
  19. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/examples.py +1 -0
  20. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/loggers.py +59 -4
  21. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/models.py +7 -2
  22. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/quizzes.py +5 -3
  23. soliplex-0.79/src/soliplex/sandbox_audit.py +164 -0
  24. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/secrets.py +8 -4
  25. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/skills/bwrap_sandbox/__init__.py +125 -85
  26. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tools/agui_run_feedback.py +1 -0
  27. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tools/rag.py +5 -5
  28. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tui/main.py +10 -0
  29. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/log_ingest.py +2 -1
  30. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/rooms.py +31 -6
  31. {soliplex-0.78.2 → soliplex-0.79/src/soliplex.egg-info}/PKG-INFO +3 -3
  32. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex.egg-info/requires.txt +1 -1
  33. soliplex-0.78.2/src/soliplex/config/rag.py +0 -198
  34. soliplex-0.78.2/src/soliplex/sandbox_audit.py +0 -65
  35. {soliplex-0.78.2 → soliplex-0.79}/LICENSE +0 -0
  36. {soliplex-0.78.2 → soliplex-0.79}/MANIFEST.in +0 -0
  37. {soliplex-0.78.2 → soliplex-0.79}/README.md +0 -0
  38. {soliplex-0.78.2 → soliplex-0.79}/example/skills/bare-bones/SKILL.md +0 -0
  39. {soliplex-0.78.2 → soliplex-0.79}/example/skills/stanzas/SKILL.md +0 -0
  40. {soliplex-0.78.2 → soliplex-0.79}/setup.cfg +0 -0
  41. {soliplex-0.78.2 → soliplex-0.79}/skills/soliplex-docs/SKILL.md +0 -0
  42. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/agents.py +0 -0
  43. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/agui/__init__.py +0 -0
  44. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/agui/parser.py +0 -0
  45. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/agui/schema.py +0 -0
  46. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/authn.py +0 -0
  47. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/authz/persistence.py +0 -0
  48. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/capabilities/filesystem.py +0 -0
  49. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/capabilities/rag_audit.py +0 -0
  50. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/admin_users.py +0 -0
  51. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/ask.py +0 -0
  52. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/cli_util.py +0 -0
  53. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/main.py +0 -0
  54. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/ollama.py +0 -0
  55. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/room_authz.py +0 -0
  56. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/serve.py +0 -0
  57. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/cli/types.py +0 -0
  58. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/completions.py +0 -0
  59. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/__init__.py +0 -0
  60. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/_utils.py +0 -0
  61. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/agui.py +0 -0
  62. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/exceptions.py +0 -0
  63. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/meta.py +0 -0
  64. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/middleware.py +0 -0
  65. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/config/routing.py +0 -0
  66. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/installation.py +0 -0
  67. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/log_ingest.py +0 -0
  68. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/main.py +0 -0
  69. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/mcp_auth.py +0 -0
  70. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/mcp_client.py +0 -0
  71. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/mcp_server.py +0 -0
  72. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/ollama.py +0 -0
  73. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/rag_audit.py +0 -0
  74. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/skills/bwrap_sandbox/SKILL.md +0 -0
  75. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/titles.py +0 -0
  76. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tools/__init__.py +0 -0
  77. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tools/file_uploads.py +0 -0
  78. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tui/cli.py +0 -0
  79. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tui/rest_api.py +0 -0
  80. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/tui/serve.py +0 -0
  81. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/util.py +0 -0
  82. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/__init__.py +0 -0
  83. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/agui.py +0 -0
  84. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/authn.py +0 -0
  85. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/authz.py +0 -0
  86. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/completions.py +0 -0
  87. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/installation.py +0 -0
  88. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/quizzes.py +0 -0
  89. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/room_file_uploads.py +0 -0
  90. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/sandbox_workdirs.py +0 -0
  91. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/stats.py +0 -0
  92. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/streaming.py +0 -0
  93. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex/views/thread_file_uploads.py +0 -0
  94. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex.egg-info/SOURCES.txt +0 -0
  95. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex.egg-info/dependency_links.txt +0 -0
  96. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex.egg-info/entry_points.txt +0 -0
  97. {soliplex-0.78.2 → soliplex-0.79}/src/soliplex.egg-info/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: soliplex
3
- Version: 0.78.2
3
+ Version: 0.79
4
4
  Summary: An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface.
5
5
  Author-email: Enfold <info@enfoldsystems.net>
6
6
  Classifier: Programming Language :: Python :: 3
7
7
  Classifier: Operating System :: OS Independent
8
- Requires-Python: >=3.12
8
+ Requires-Python: >=3.13
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: ag-ui-protocol<0.1.19,>=0.1.18
@@ -19,7 +19,7 @@ Requires-Dist: fastapi
19
19
  Requires-Dist: fastmcp<3.4,>=3.3.0
20
20
  Requires-Dist: fakeredis!=2.35.0
21
21
  Requires-Dist: greenlet
22
- Requires-Dist: haiku.rag-slim<0.79,>=0.78.0
22
+ Requires-Dist: haiku.rag-slim<0.83,>=0.82.0
23
23
  Requires-Dist: idna>=3.15
24
24
  Requires-Dist: itsdangerous
25
25
  Requires-Dist: joserfc<1.7,>=1.6.7
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "soliplex"
7
- version = "0.78.2"
7
+ version = "0.79"
8
8
  description = "An AI-powered Retrieval-Augmented Generation (RAG) system with a modern web interface."
9
9
  authors = [{ name = "Enfold", email = "info@enfoldsystems.net" }]
10
10
  readme = "README.md"
11
- requires-python = ">=3.12"
11
+ requires-python = ">=3.13"
12
12
  classifiers = [
13
13
  "Programming Language :: Python :: 3",
14
14
  "Operating System :: OS Independent",
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "fastmcp >= 3.3.0, < 3.4",
26
26
  "fakeredis != 2.35.0", # brownbag
27
27
  "greenlet",
28
- "haiku.rag-slim >= 0.78.0, < 0.79",
28
+ "haiku.rag-slim >= 0.82.0, < 0.83",
29
29
  "idna >= 3.15",
30
30
  "itsdangerous",
31
31
  "joserfc >= 1.6.7, < 1.7", # CVE-2026-48990
@@ -84,7 +84,13 @@ pythonpath = [".", "tests/unit"]
84
84
  python_files = "test_*.py"
85
85
  # skip 'tests/functional' by default
86
86
  testpaths = ["tests/unit"]
87
- addopts = "--cov=soliplex --cov=tests/unit --cov=scripts --cov=skills/soliplex-docs/scripts --cov-branch --cov-fail-under=100"
87
+ # 'src/soliplex' (a path), not 'soliplex' (the importable name):
88
+ # 'soliplex' is a namespace package, and coverage cannot enumerate one
89
+ # by walking the filesystem, so a package-name target silently measures
90
+ # only the modules some test happened to import. A path target reports
91
+ # never-imported modules at 0%, which is what makes 'omit' below the
92
+ # single place that decides what is exempt.
93
+ addopts = "--cov=src/soliplex --cov=tests/unit --cov=scripts --cov=skills/soliplex-docs/scripts --cov-branch --cov-fail-under=100"
88
94
  filterwarnings = [
89
95
  # "ignore::DeprecationWarning:<source-package>",
90
96
  ]
@@ -95,8 +101,7 @@ markers = [
95
101
  [tool.coverage.run]
96
102
  omit = [
97
103
  "scripts/lint_textio.py", # runs its own '--self-test' during lint
98
- "src/soliplex/cli/__init__.py",
99
- "src/soliplex/tui.py",
104
+ "src/soliplex/tui/*", # the TUI is deliberately untested
100
105
  ]
101
106
 
102
107
  [tool.coverage.report]
@@ -16,9 +16,6 @@ from soliplex.agui import persistence as agui_persistence
16
16
  from soliplex.agui import schema as agui_schema
17
17
  from soliplex.agui import util as agui_util
18
18
 
19
- # Temporary backward-compatibility: to be removed in 'v0.45'
20
- from soliplex.agui.schema import * # noqa F403
21
-
22
19
  FeedbackReviewStatus = agui.FeedbackReviewStatus
23
20
 
24
21
 
@@ -7,4 +7,4 @@ def _make_uuid_str() -> str:
7
7
 
8
8
 
9
9
  def _timestamp() -> datetime.datetime:
10
- return datetime.datetime.now(datetime.timezone.utc) # noqa UP07
10
+ return datetime.datetime.now(datetime.UTC)
@@ -9,7 +9,9 @@ import typing
9
9
  import jsonpath
10
10
 
11
11
  # Avoid circular import when only used for typing
12
- # from soliplex import models
12
+
13
+ if typing.TYPE_CHECKING: # avoid an import cycle at runtime
14
+ from soliplex import models
13
15
 
14
16
  UserToken = dict[str, typing.Any]
15
17
 
@@ -249,14 +251,14 @@ class RoomAuthorizationPolicy(abc.ABC):
249
251
  async def get_room_policy(
250
252
  self,
251
253
  room_id: str,
252
- ) -> models.RoomPolicy | None: # noqa: F821
254
+ ) -> models.RoomPolicy | None:
253
255
  """Return the authorization policy for the room"""
254
256
 
255
257
  @abc.abstractmethod
256
258
  async def update_room_policy(
257
259
  self,
258
260
  room_id: str,
259
- room_policy: models.RoomPolicy, # noqa: F821
261
+ room_policy: models.RoomPolicy,
260
262
  ) -> None:
261
263
  """Update the authorization policy for the room"""
262
264
 
@@ -271,13 +273,13 @@ class RoomAuthorizationPolicy(abc.ABC):
271
273
  async def get_room_policy_unchecked(
272
274
  self,
273
275
  room_id: str,
274
- ) -> models.RoomPolicyUnchecked | None: # noqa: F821
276
+ ) -> models.RoomPolicyUnchecked | None:
275
277
  """Return the room policy, tolerating non-compiling 'json_path's."""
276
278
 
277
279
  @abc.abstractmethod
278
280
  async def list_room_policies(
279
281
  self,
280
- ) -> list[models.RoomPolicyUnchecked]: # noqa: F821
282
+ ) -> list[models.RoomPolicyUnchecked]:
281
283
  """List every stored room policy as an unchecked model.
282
284
 
283
285
  Each policy is returned as the unchecked model -- the analogue
@@ -298,7 +300,7 @@ class RoomAuthorizationPolicy(abc.ABC):
298
300
  async def add_acl_entry(
299
301
  self,
300
302
  room_id: str,
301
- entry: models.ACLEntry, # noqa: F821
303
+ entry: models.ACLEntry,
302
304
  ) -> None:
303
305
  """Add an ACL entry to the room's policy, replacing any entry with
304
306
  the same discriminator."""
@@ -307,7 +309,7 @@ class RoomAuthorizationPolicy(abc.ABC):
307
309
  async def remove_acl_entry(
308
310
  self,
309
311
  room_id: str,
310
- entry: models.ACLEntryUnchecked, # noqa: F821
312
+ entry: models.ACLEntryUnchecked,
311
313
  ) -> None:
312
314
  """Remove the matching ACL entry from the room's policy."""
313
315
 
@@ -23,7 +23,7 @@ relationship = sqla_orm.relationship
23
23
 
24
24
 
25
25
  def _timestamp() -> datetime.datetime:
26
- return datetime.datetime.now(datetime.timezone.utc) # noqa UP07
26
+ return datetime.datetime.now(datetime.UTC)
27
27
 
28
28
 
29
29
  # Recommended naming convention used by Alembic, as various different database
@@ -408,6 +408,24 @@ def _iter_room_rag_candidates(room_config):
408
408
  yield f"tool:{tool_config.tool_name}", tool_config
409
409
 
410
410
 
411
+ def _rag_db_probes(source, cfg):
412
+ """Yield ``(label, cfg)`` for each database a config names.
413
+
414
+ A probe checks a path this config places, so a config deferring
415
+ placement to its haiku.rag config yields nothing: opening it for the
416
+ document count is what checks it. A candidate is only known to expose
417
+ ``haiku_rag_config``, so one that cannot place a database at all yields
418
+ itself and reports its own error from inside the caller's ``try``.
419
+ """
420
+ rag_databases = getattr(cfg, "rag_databases", None)
421
+
422
+ if rag_databases:
423
+ for entry in rag_databases:
424
+ yield f"{source}#{entry.name}", entry
425
+ elif getattr(cfg, "names_own_databases", True):
426
+ yield source, cfg
427
+
428
+
411
429
  def _invalid_room_agui_features(
412
430
  the_installation: installation.Installation,
413
431
  ) -> dict:
@@ -438,9 +456,18 @@ def _invalid_room_rag_dbs(
438
456
 
439
457
  for source, cfg in _iter_room_rag_candidates(room_config):
440
458
  try:
441
- cfg.rag_lancedb_path # noqa B018
459
+ _ = cfg.haiku_rag_config # property raises
442
460
  except Exception as exc:
461
+ # A config that will not build says why; what it
462
+ # names is unanswerable until that is fixed.
443
463
  per_room[source] = str(exc)
464
+ continue
465
+
466
+ for label, probe in _rag_db_probes(source, cfg):
467
+ try:
468
+ _ = probe.rag_lancedb_path # property raises
469
+ except Exception as exc:
470
+ per_room[label] = str(exc)
444
471
 
445
472
  if per_room:
446
473
  rag_errors[room_config.id] = per_room
@@ -505,12 +532,24 @@ def _audit_rooms_section(
505
532
  tc_print()
506
533
  tc_print(" Haiku Rag DBs")
507
534
  for source, cfg in candidates:
508
- exc = per_room_rag.get(source)
509
- if exc is not None:
510
- tc_print(f" - {source:20}: ERROR: {exc}")
535
+ # A config that will not build is recorded under its own
536
+ # source, which the per-database labels never match.
537
+ if source in per_room_rag:
538
+ failed = (source, per_room_rag[source])
539
+ else:
540
+ failed = next(
541
+ (
542
+ (label, per_room_rag[label])
543
+ for label, _ in _rag_db_probes(source, cfg)
544
+ if label in per_room_rag
545
+ ),
546
+ None,
547
+ )
548
+ if failed is not None:
549
+ failed_label, exc = failed
550
+ tc_print(f" - {failed_label:20}: ERROR: {exc}")
511
551
  else:
512
552
  rag = hr_client.HaikuRAG(
513
- db_path=cfg.rag_lancedb_path,
514
553
  config=cfg.haiku_rag_config,
515
554
  read_only=True,
516
555
  )
@@ -522,7 +561,7 @@ def _audit_rooms_section(
522
561
  room_rag_errors[source] = count_error
523
562
  tc_print(
524
563
  f" - {source:20}: "
525
- f"{str(cfg.rag_lancedb_path):30} {count_display}"
564
+ f"{cfg.rag_db_audit_path:30} {count_display}"
526
565
  )
527
566
  tc_print()
528
567
  tc_line()
@@ -21,6 +21,9 @@ from pydantic_ai.providers import openai as openai_providers
21
21
  from . import _utils
22
22
  from . import exceptions
23
23
 
24
+ if typing.TYPE_CHECKING: # avoid an import cycle at runtime
25
+ from . import installation as config_installation
26
+
24
27
  _no_repr_no_compare_none = _utils._no_repr_no_compare_none
25
28
  _default_dict_field = _utils._default_dict_field
26
29
  _default_list_field = _utils._default_list_field
@@ -186,7 +189,7 @@ class AgentConfig:
186
189
  agui_feature_names: tuple[str] = ()
187
190
 
188
191
  # Set by `from_yaml` factory
189
- _installation_config: InstallationConfig = ( # noqa F821 cycle
192
+ _installation_config: config_installation.InstallationConfig = (
190
193
  _no_repr_no_compare_none()
191
194
  )
192
195
  _config_path: pathlib.Path = None
@@ -207,7 +210,7 @@ class AgentConfig:
207
210
  @classmethod
208
211
  def from_yaml(
209
212
  cls,
210
- installation_config: InstallationConfig, # noqa F821 cycle
213
+ installation_config: config_installation.InstallationConfig,
211
214
  config_path: pathlib.Path,
212
215
  config_dict: dict,
213
216
  ):
@@ -362,7 +365,7 @@ class FactoryAgentConfig:
362
365
  _factory: AgentFactory = None
363
366
 
364
367
  # Set by `from_yaml` factory
365
- _installation_config: InstallationConfig = ( # noqa F821 cycle
368
+ _installation_config: config_installation.InstallationConfig = (
366
369
  _no_repr_no_compare_none()
367
370
  )
368
371
  _config_path: pathlib.Path = None
@@ -394,7 +397,7 @@ class FactoryAgentConfig:
394
397
  @classmethod
395
398
  def from_yaml(
396
399
  cls,
397
- installation_config: InstallationConfig, # noqa F821 cycle
400
+ installation_config: config_installation.InstallationConfig,
398
401
  config_path: pathlib.Path,
399
402
  config_dict: dict,
400
403
  ):
@@ -449,7 +452,7 @@ AgentConfigMap = dict[str, AgentConfigTypes]
449
452
 
450
453
 
451
454
  def extract_agent_config(
452
- installation_config: InstallationConfig, # noqa F821 cycle
455
+ installation_config: config_installation.InstallationConfig,
453
456
  config_path: pathlib.Path,
454
457
  config_dict: dict,
455
458
  ) -> AgentConfig: # or subclass
@@ -8,6 +8,9 @@ import typing
8
8
  from . import _utils
9
9
  from . import exceptions as config_exc
10
10
 
11
+ if typing.TYPE_CHECKING: # avoid an import cycle at runtime
12
+ from . import installation as config_installation
13
+
11
14
  _no_repr_no_compare_none = _utils._no_repr_no_compare_none
12
15
 
13
16
 
@@ -31,7 +34,7 @@ class OIDCAuthSystemConfig:
31
34
  oidc_client_pem_path: pathlib.Path = None
32
35
 
33
36
  # Set in 'from_yaml' below
34
- _installation_config: InstallationConfig = ( # noqa F821 cycles
37
+ _installation_config: config_installation.InstallationConfig = (
35
38
  _no_repr_no_compare_none()
36
39
  )
37
40
  _config_path: pathlib.Path = None
@@ -39,7 +42,7 @@ class OIDCAuthSystemConfig:
39
42
  @classmethod
40
43
  def from_yaml(
41
44
  cls,
42
- installation_config: InstallationConfig, # noqa F821 cycles
45
+ installation_config: config_installation.InstallationConfig,
43
46
  config_path: pathlib.Path,
44
47
  config_dict: dict[str, typing.Any],
45
48
  ):
@@ -2,11 +2,15 @@ from __future__ import annotations # forward refs in typing decls
2
2
 
3
3
  import dataclasses
4
4
  import pathlib
5
+ import typing
5
6
 
6
7
  from . import _utils
7
8
  from . import agents as config_agents
8
9
  from . import tools as config_tools
9
10
 
11
+ if typing.TYPE_CHECKING: # avoid an import cycle at runtime
12
+ from . import installation as config_installation
13
+
10
14
  _no_repr_no_compare_none = _utils._no_repr_no_compare_none
11
15
  _default_dict_field = _utils._default_dict_field
12
16
 
@@ -36,7 +40,7 @@ class CompletionConfig:
36
40
  )
37
41
 
38
42
  # Set by `from_yaml` factory
39
- _installation_config: InstallationConfig = ( # noqa F821 cycles
43
+ _installation_config: config_installation.InstallationConfig = (
40
44
  _no_repr_no_compare_none()
41
45
  )
42
46
  _config_path: pathlib.Path = None
@@ -44,7 +48,7 @@ class CompletionConfig:
44
48
  @classmethod
45
49
  def from_yaml(
46
50
  cls,
47
- installation_config: InstallationConfig, # noqa F821 cycles
51
+ installation_config: config_installation.InstallationConfig,
48
52
  config_path: pathlib.Path,
49
53
  config_dict: dict,
50
54
  ):
@@ -7,7 +7,6 @@ import itertools
7
7
  import os
8
8
  import pathlib
9
9
  import re
10
- import sys
11
10
  import typing
12
11
 
13
12
  import dotenv
@@ -148,10 +147,28 @@ def _find_configs_yaml(
148
147
  yield sub_config, _load_config_yaml(sub_config)
149
148
  except config_exc.NoSuchConfig:
150
149
  continue
151
- else: # pragma: NO COVER
150
+ else:
152
151
  pass
153
152
 
154
153
 
154
+ def _resolved_tokens(value, split_re, resolve):
155
+ """Yield 'value' tokenized on 'split_re', resolving the markers.
156
+
157
+ 'split_re' has a single capturing group, so 'split' alternates literal
158
+ text with captured marker names. Batching the tokens in pairs makes
159
+ the second element of each pair (when present) the marker to resolve.
160
+ """
161
+ tokens = split_re.split(value)
162
+
163
+ batch = itertools.batched(tokens, 2, strict=False)
164
+
165
+ for two_or_one in batch:
166
+ yield two_or_one[0]
167
+
168
+ if len(two_or_one) == 2:
169
+ yield resolve(two_or_one[1])
170
+
171
+
155
172
  _find_room_configs = functools.partial(
156
173
  _find_configs_yaml,
157
174
  filename_yaml="room_config.yaml",
@@ -443,22 +460,13 @@ class InstallationConfig:
443
460
 
444
461
  The marker pattern may appear zero or more times.
445
462
  """
446
-
447
- def resolved_tokens(value):
448
- tokens = config_secrets.SECRET_RE.split(value)
449
-
450
- if sys.version_info >= (3, 13): # noqa: UP036
451
- batch = itertools.batched(tokens, 2, strict=False)
452
- else: # pragma: NO COVER
453
- batch = itertools.batched(tokens, 2) # noqa: B911
454
-
455
- for two_or_one in batch:
456
- yield two_or_one[0]
457
-
458
- if len(two_or_one) == 2:
459
- yield self._resolve_secret(two_or_one[1])
460
-
461
- return "".join(resolved_tokens(value))
463
+ return "".join(
464
+ _resolved_tokens(
465
+ value,
466
+ config_secrets.SECRET_RE,
467
+ self._resolve_secret,
468
+ )
469
+ )
462
470
 
463
471
  #
464
472
  # Map values similar to 'os.environ'.
@@ -489,7 +497,7 @@ class InstallationConfig:
489
497
 
490
498
  if from_dotenv is not None:
491
499
  result.append(ES(EST.DOT_ENV, from_dotenv))
492
- else: # pragma: NO COVER
500
+ else:
493
501
  pass
494
502
 
495
503
  from_osenv = os.getenv(key)
@@ -540,23 +548,14 @@ class InstallationConfig:
540
548
 
541
549
  The marker pattern may appear zero or more times.
542
550
  """
543
-
544
- def resolved_tokens(value):
545
- tokens = ENVIRONMENT_RE.split(value)
546
-
547
- if sys.version_info >= (3, 13): # noqa: UP036
548
- batch = itertools.batched(tokens, 2, strict=False)
549
- else: # pragma: NO COVER
550
- batch = itertools.batched(tokens, 2) # noqa: B911
551
-
552
- for two_or_one in batch:
553
- yield two_or_one[0]
554
-
555
- if len(two_or_one) == 2:
556
- yield self._resolve_environment_var(two_or_one[1])
557
-
558
551
  if isinstance(value, str):
559
- return "".join(resolved_tokens(value))
552
+ return "".join(
553
+ _resolved_tokens(
554
+ value,
555
+ ENVIRONMENT_RE,
556
+ self._resolve_environment_var,
557
+ )
558
+ )
560
559
  else:
561
560
  return value
562
561
 
@@ -962,7 +961,7 @@ class InstallationConfig:
962
961
 
963
962
  return cls(**config_dict)
964
963
 
965
- except config_exc.FromYamlException: # pragma: NO COVER
964
+ except config_exc.FromYamlException:
966
965
  raise
967
966
 
968
967
  except Exception as exc:
@@ -9,6 +9,9 @@ import logfire
9
9
  from . import _utils
10
10
  from . import exceptions as config_exc
11
11
 
12
+ if typing.TYPE_CHECKING: # avoid an import cycle at runtime
13
+ from . import installation as config_installation
14
+
12
15
  _no_repr_no_compare_none = _utils._no_repr_no_compare_none
13
16
 
14
17
 
@@ -117,7 +120,7 @@ class LogfireConfig:
117
120
  instrument_fast_api: LogfireInstrumentFastAPI = None
118
121
 
119
122
  # Set by `from_yaml` factory
120
- _installation_config: InstallationConfig = ( # noqa F821 cycles
123
+ _installation_config: config_installation.InstallationConfig = (
121
124
  _no_repr_no_compare_none()
122
125
  )
123
126
  _config_path: pathlib.Path = None
@@ -208,7 +211,7 @@ class LogfireConfig:
208
211
  @classmethod
209
212
  def from_yaml(
210
213
  cls,
211
- installation_config: InstallationConfig, # noqa F821 cycles
214
+ installation_config: config_installation.InstallationConfig,
212
215
  config_path: pathlib.Path,
213
216
  config_dict: dict,
214
217
  ):
@@ -6,11 +6,15 @@ import json
6
6
  import os
7
7
  import pathlib
8
8
  import random
9
+ import typing
9
10
 
10
11
  from . import _utils
11
12
  from . import agents
12
13
  from . import exceptions
13
14
 
15
+ if typing.TYPE_CHECKING: # avoid an import cycle at runtime
16
+ from . import installation as config_installation
17
+
14
18
  _no_repr_no_compare_none = _utils._no_repr_no_compare_none
15
19
  _default_list_field = _utils._default_list_field
16
20
  _default_dict_field = _utils._default_dict_field
@@ -87,7 +91,7 @@ class QuizConfig:
87
91
  judge_agent: agents.AgentConfig | None = None
88
92
 
89
93
  # Set by `from_yaml` factory
90
- _installation_config: InstallationConfig = ( # noqa F821 cycles
94
+ _installation_config: config_installation.InstallationConfig = (
91
95
  _no_repr_no_compare_none()
92
96
  )
93
97
  _config_path: pathlib.Path = None
@@ -95,7 +99,7 @@ class QuizConfig:
95
99
  @classmethod
96
100
  def from_yaml(
97
101
  cls,
98
- installation_config: InstallationConfig, # noqa F821 cycles
102
+ installation_config: config_installation.InstallationConfig,
99
103
  config_path: pathlib.Path,
100
104
  config_dict: dict,
101
105
  ):