pathling 9.8.0.dev0__tar.gz → 9.9.0.dev0__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 (54) hide show
  1. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/LICENSE +8 -2
  2. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/PKG-INFO +1 -1
  3. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/__init__.py +7 -1
  4. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/_version.py +2 -2
  5. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/__init__.py +2 -0
  6. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/config.py +215 -3
  7. pathling-9.9.0.dev0/pathling/cli/console.py +115 -0
  8. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/convert.py +21 -2
  9. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/departition.py +11 -4
  10. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/errors.py +53 -11
  11. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/export.py +33 -22
  12. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/fhirpath.py +48 -26
  13. pathling-9.9.0.dev0/pathling/cli/import_terminology.py +175 -0
  14. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/io.py +10 -2
  15. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/main.py +50 -14
  16. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/render.py +222 -20
  17. pathling-9.9.0.dev0/pathling/cli/ruff.toml +17 -0
  18. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/run.py +45 -16
  19. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/session.py +56 -4
  20. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/sparkconf.py +5 -3
  21. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/terminology.py +371 -172
  22. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/view.py +24 -14
  23. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/context.py +137 -4
  24. pathling-9.8.0.dev0/pathling/cli/console.py +0 -82
  25. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/.gitignore +0 -0
  26. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/README.md +0 -0
  27. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/bulk.py +0 -0
  28. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/data/bundles/Bennett146_Swaniawski813_704c9750-f6e6-473b-ee83-fbd48e07fe3f.json +0 -0
  29. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/data/bundles/Dino214_Parisian75_40d82b80-b682-cd8b-da6d-396809878641.json +0 -0
  30. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/data/csv/conditions.csv +0 -0
  31. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/data/resources/Condition.ndjson +0 -0
  32. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/data/resources/Patient.ndjson +0 -0
  33. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/designation.py +0 -0
  34. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/display.py +0 -0
  35. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/encode_bundles.py +0 -0
  36. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/encode_resources.py +0 -0
  37. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/fhir_search.py +0 -0
  38. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/fhir_view.py +0 -0
  39. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/member_of.py +0 -0
  40. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/property_of.py +0 -0
  41. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/subsumes.py +0 -0
  42. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/examples/translate.py +0 -0
  43. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/_spark_defaults.py +0 -0
  44. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/bulk.py +0 -0
  45. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/cli/resources/quiet-log4j2.properties +0 -0
  46. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/coding.py +0 -0
  47. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/core.py +0 -0
  48. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/datasink.py +0 -0
  49. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/datasource.py +0 -0
  50. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/fhir.py +0 -0
  51. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/functions.py +0 -0
  52. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/spark.py +0 -0
  53. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pathling/udfs.py +0 -0
  54. {pathling-9.8.0.dev0 → pathling-9.9.0.dev0}/pyproject.toml +0 -0
@@ -183,24 +183,28 @@ agree to comply with the licence terms for these components as part of
183
183
  accessing the Software. Other third party software may also be identified in
184
184
  separate files distributed with the Software.
185
185
 
186
- * (Apache License, Version 2.0) FHIR Bulk Client (au.csiro.fhir:bulk-export:1.0.4 - https://github.com/aehrc/fhir-bulk-java)
186
+ * (Apache License, Version 2.0) FHIR Bulk Client (au.csiro.fhir:bulk-export:1.1.0 - https://github.com/aehrc/fhir-bulk-java)
187
187
  * (Apache License, Version 2.0) FHIR Auth Client for Java (au.csiro.fhir:fhir-auth:1.0.0 - https://github.com/aehrc/fhir-auth-java)
188
188
  * (Apache License, Version 2.0) HAPI FHIR - Core Library (ca.uhn.hapi.fhir:hapi-fhir-base:8.10.0 - https://hapifhir.io/)
189
189
  * (Apache License, Version 2.0) HAPI FHIR - Client Framework (ca.uhn.hapi.fhir:hapi-fhir-client:8.10.0 - https://hapifhir.io/hapi-deployable-pom/hapi-fhir-client)
190
190
  * (Apache License, Version 2.0) HAPI FHIR Structures - FHIR R4 (ca.uhn.hapi.fhir:hapi-fhir-structures-r4:8.10.0 - https://hapifhir.io/hapi-deployable-pom/hapi-fhir-structures-r4)
191
- * (Eclipse Public License 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.5.25 - http://logback.qos.ch/logback-classic)
191
+ * (EPL-2.0) (LGPL-2.1-only) Logback Classic Module (ch.qos.logback:logback-classic:1.5.38 - http://logback.qos.ch/logback-classic)
192
+ * (Apache License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.22.1 - https://github.com/FasterXML/jackson-core)
192
193
  * (Apache License, Version 2.0) WireMock (com.github.tomakehurst:wiremock-jre8-standalone:2.35.2 - http://wiremock.org)
193
194
  * (Apache License, Version 2.0) FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/)
194
195
  * (Apache License, Version 2.0) Gson (com.google.code.gson:gson:2.10 - https://github.com/google/gson/gson)
195
196
  * (Apache License, Version 2.0) Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.11.0 - https://commons.apache.org/proper/commons-beanutils)
196
197
  * (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.16.1 - https://commons.apache.org/proper/commons-io/)
197
198
  * (Apache License, Version 2.0) Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/)
199
+ * (Apache License, Version 2.0) delta-kernel-api (io.delta:delta-kernel-api:4.0.0 - https://delta.io/)
200
+ * (Apache License, Version 2.0) delta-kernel-defaults (io.delta:delta-kernel-defaults:4.0.0 - https://delta.io/)
198
201
  * (Apache License, Version 2.0) delta-spark (io.delta:delta-spark_2.13:4.0.0 - https://delta.io/)
199
202
  * (Apache License, Version 2.0) ucumate-core (io.github.fhnaumann:ucumate-core:1.0.8 - https://github.com/fhnaumann/ucumate)
200
203
  * (EPL 2.0) (GPL2 w/ CPE) Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:5.0.0 - https://projects.eclipse.org/projects/ee4j.servlet)
201
204
  * (Apache License, Version 2.0) Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org)
202
205
  * (Apache License, Version 2.0) Joda-Time (joda-time:joda-time:2.12.7 - https://www.joda.org/joda-time/)
203
206
  * (BSD License) ANTLR 4 Tool (org.antlr:antlr4:4.13.1 - http://www.antlr.org)
207
+ * (BSD License) ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.13.1 - https://www.antlr.org/antlr4-runtime/)
204
208
  * (Apache License, Version 2.0) Apache Commons Lang (org.apache.commons:commons-lang3:3.18.0 - https://commons.apache.org/proper/commons-lang/)
205
209
  * (Apache License, Version 2.0) Apache Derby Tools (org.apache.derby:derbytools:10.16.1.1 - http://db.apache.org/derby/)
206
210
  * (Apache License, Version 2.0) Apache Hadoop Client API (org.apache.hadoop:hadoop-client-api:3.4.1 - no url defined)
@@ -222,7 +226,9 @@ separate files distributed with the Software.
222
226
  * (MIT License) mockito-core (org.mockito:mockito-core:5.18.0 - https://github.com/mockito/mockito)
223
227
  * (GNU General Public License (GPL), version 2, with the Classpath exception) JMH Core (org.openjdk.jmh:jmh-core:1.37 - http://openjdk.java.net/projects/code-tools/jmh/jmh-core/)
224
228
  * (GNU General Public License (GPL), version 2, with the Classpath exception) JMH Generators: Annotation Processors (org.openjdk.jmh:jmh-generator-annprocess:1.37 - http://openjdk.java.net/projects/code-tools/jmh/jmh-generator-annprocess/)
229
+ * (GNU General Public License (GPL), version 2, with the Classpath exception) Java Object Layout: Core (org.openjdk.jol:jol-core:0.17 - http://maven.apache.org)
225
230
  * (MIT License) Project Lombok (org.projectlombok:lombok:1.18.38 - https://projectlombok.org)
231
+ * (Apache License, Version 2.0) org.roaringbitmap:RoaringBitmap (org.roaringbitmap:RoaringBitmap:1.3.0 - https://github.com/RoaringBitmap/RoaringBitmap)
226
232
  * (Apache License, Version 2.0) Scala Library (org.scala-lang:scala-library:2.13.16 - https://www.scala-lang.org/)
227
233
  * (Apache License, Version 2.0) JSONassert (org.skyscreamer:jsonassert:1.5.1 - https://github.com/skyscreamer/JSONassert)
228
234
  * (Apache License, Version 2.0) JSONassert (org.skyscreamer:jsonassert:1.5.3 - https://github.com/skyscreamer/JSONassert)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathling
3
- Version: 9.8.0.dev0
3
+ Version: 9.9.0.dev0
4
4
  Summary: Python API for Pathling
5
5
  Project-URL: Homepage, https://github.com/aehrc/pathling
6
6
  Author-email: "Australian e-Health Research Centre, CSIRO" <pathling@csiro.au>
@@ -39,7 +39,12 @@ if TYPE_CHECKING:
39
39
  from .core import Expression, VariableExpression
40
40
  from .datasource import DataSource, DataSources
41
41
  from .fhir import MimeType, Version
42
- from .functions import to_coding, to_ecl_value_set, to_snomed_coding
42
+ from .functions import (
43
+ to_coding,
44
+ to_ecl_value_set,
45
+ to_loinc_coding,
46
+ to_snomed_coding,
47
+ )
43
48
  from .udfs import (
44
49
  Equivalence,
45
50
  PropertyType,
@@ -65,6 +70,7 @@ _LAZY_EXPORTS = {
65
70
  "Version": "pathling.fhir",
66
71
  "to_coding": "pathling.functions",
67
72
  "to_snomed_coding": "pathling.functions",
73
+ "to_loinc_coding": "pathling.functions",
68
74
  "to_ecl_value_set": "pathling.functions",
69
75
  "member_of": "pathling.udfs",
70
76
  "translate": "pathling.udfs",
@@ -2,8 +2,8 @@
2
2
  # Auto generated from POM project version.
3
3
  # Please do not modify.
4
4
  #
5
- __version__="9.8.0.dev0"
6
- __java_version__="9.8.0-SNAPSHOT"
5
+ __version__="9.9.0.dev0"
6
+ __java_version__="9.9.0-SNAPSHOT"
7
7
  __scala_version__="2.13"
8
8
  __delta_version__="4.0.0"
9
9
  __hadoop_version__="3.4.1"
@@ -25,3 +25,5 @@ command needs it.
25
25
 
26
26
  Author: John Grimes.
27
27
  """
28
+
29
+ from __future__ import annotations
@@ -30,6 +30,8 @@ reference, or via an environment variable.
30
30
  Author: John Grimes.
31
31
  """
32
32
 
33
+ from __future__ import annotations
34
+
33
35
  import os
34
36
  import sys
35
37
  from dataclasses import dataclass, field
@@ -53,7 +55,7 @@ PROJECT_CONFIG_FILENAME = "pathling.toml"
53
55
 
54
56
  # Valid top-level keys in the config file.
55
57
  VALID_CONFIG_KEYS = frozenset(
56
- {"tx-server", "fhir-version", "terminology-auth", "bulk-auth", "spark"}
58
+ {"tx-server", "fhir-version", "terminology-auth", "bulk-auth", "spark", "tx-store"}
57
59
  )
58
60
 
59
61
  # Valid keys within the [terminology-auth] and [bulk-auth] tables.
@@ -61,6 +63,17 @@ VALID_AUTH_KEYS = frozenset(
61
63
  {"client-id", "client-secret", "private-key-jwk", "token-endpoint", "scope"}
62
64
  )
63
65
 
66
+ # Valid keys within the [tx-store] table.
67
+ VALID_TX_STORE_KEYS = frozenset(
68
+ {
69
+ "path",
70
+ "default-snomed-edition",
71
+ "expansion-cache-size",
72
+ "dialect-aliases",
73
+ "default-dialect",
74
+ }
75
+ )
76
+
64
77
 
65
78
  @dataclass
66
79
  class TxAuth:
@@ -116,6 +129,37 @@ class BulkAuth:
116
129
  return "no credential"
117
130
 
118
131
 
132
+ @dataclass
133
+ class TxStore:
134
+ """Resolved local terminology store settings for a single invocation.
135
+
136
+ A ``TxStore`` exists only when a store path was supplied (via the
137
+ ``--tx-store`` flag or the ``tx-store.path`` config key); its presence on
138
+ :class:`CliConfig` is what selects local terminology mode. Tuning fields
139
+ left unset fall through to the library defaults.
140
+
141
+ :param path: the store location; a filesystem path or a URI. Not checked for
142
+ existence.
143
+ :param default_snomed_edition: the SNOMED CT module identifier used to
144
+ disambiguate an ambiguous edition, or None to use the library default.
145
+ :param expansion_cache_size: the per-executor value set expansion cache size,
146
+ or None to use the library default.
147
+ :param dialect_aliases: additional dialect tags recognised when a display or
148
+ designation is requested in a particular language, mapping a language
149
+ tag to a SNOMED CT language reference set identifier, or None.
150
+ :param default_dialect: the dialect whose preferred synonyms become the
151
+ stored display when ``import-snomed`` runs without a
152
+ ``--default-dialect`` flag: a tag such as ``en-AU``, or a language
153
+ reference set identifier. None when unset.
154
+ """
155
+
156
+ path: str
157
+ default_snomed_edition: Optional[str] = None
158
+ expansion_cache_size: Optional[int] = None
159
+ dialect_aliases: Optional[dict] = None
160
+ default_dialect: Optional[str] = None
161
+
162
+
119
163
  @dataclass
120
164
  class CliConfig:
121
165
  """Resolved global configuration for a single invocation.
@@ -131,6 +175,12 @@ class CliConfig:
131
175
  config file, or None when absent. Carried so ``export`` resolves bulk
132
176
  credentials from the already-loaded config rather than re-reading a
133
177
  file.
178
+ :param tx_store: the resolved local terminology store settings, or None. When
179
+ present, terminology evaluation uses the local store rather than a
180
+ remote server.
181
+ :param tx_server_explicit: whether the terminology server URL was set
182
+ explicitly (via flag or config key) rather than falling back to the
183
+ built-in default. Drives the store-wins conflict warning.
134
184
  """
135
185
 
136
186
  tx_server: str = DEFAULT_TX_SERVER
@@ -140,6 +190,8 @@ class CliConfig:
140
190
  config_path: Optional[Path] = None
141
191
  spark_conf: dict = field(default_factory=dict)
142
192
  bulk_auth_table: Optional[dict] = None
193
+ tx_store: Optional[TxStore] = None
194
+ tx_server_explicit: bool = False
143
195
 
144
196
 
145
197
  def _load_toml(path: Path) -> dict:
@@ -256,6 +308,23 @@ def load_config_file(
256
308
  f"Ignoring unknown config key '{table_name}.{key}' in "
257
309
  f"{path}. Valid keys are: {valid_auth}."
258
310
  )
311
+ tx_store_table = data.get("tx-store")
312
+ if isinstance(tx_store_table, dict):
313
+ valid_tx_store = ", ".join(sorted(VALID_TX_STORE_KEYS))
314
+ for key in tx_store_table:
315
+ if key not in VALID_TX_STORE_KEYS:
316
+ warn(
317
+ f"Ignoring unknown config key 'tx-store.{key}' in {path}. "
318
+ f"Valid keys are: {valid_tx_store}."
319
+ )
320
+ elif tx_store_table is not None:
321
+ # A scalar tx-store value cannot configure local mode; warn rather than
322
+ # silently ignoring it (a common mistake is 'tx-store = \"/path\"'
323
+ # instead of a [tx-store] table with a 'path' key).
324
+ warn(
325
+ f"Ignoring 'tx-store' in {path}; it must be a [tx-store] table with "
326
+ "a 'path' key, not a single value."
327
+ )
259
328
  return data
260
329
 
261
330
 
@@ -385,6 +454,117 @@ def resolve_bulk_auth(
385
454
  )
386
455
 
387
456
 
457
+ def _resolve_tx_store(
458
+ file_data: dict,
459
+ tx_store_flag: Optional[str],
460
+ on_warning: Optional[Callable[[str], None]],
461
+ ) -> Optional[TxStore]:
462
+ """Resolves and validates the local terminology store settings.
463
+
464
+ The store path follows the precedence ``--tx-store`` flag >
465
+ ``tx-store.path`` config key; the tuning keys come from the config table
466
+ only. All type and range checks happen here, before any Spark session, so a
467
+ configuration mistake fails fast (FR-008).
468
+
469
+ :param file_data: the parsed config file contents.
470
+ :param tx_store_flag: the ``--tx-store`` flag value, or None.
471
+ :param on_warning: the warning callback for the path-less-table notice.
472
+ :return: a populated :class:`TxStore`, or None when no store is configured.
473
+ :raises CliError: with EXIT_USAGE for an empty flag or an invalid table value.
474
+ """
475
+ from pathling.cli.errors import EXIT_USAGE, CliError
476
+
477
+ # An empty flag value is a usage error rather than a silent no-op (FR-008).
478
+ if tx_store_flag is not None and not tx_store_flag:
479
+ raise CliError(
480
+ "The --tx-store value must not be empty. Provide a store path or URI.",
481
+ exit_code=EXIT_USAGE,
482
+ )
483
+
484
+ table = file_data.get("tx-store")
485
+ table = table if isinstance(table, dict) else {}
486
+
487
+ config_path = table.get("path")
488
+ if config_path is not None and not isinstance(config_path, str):
489
+ raise CliError(
490
+ "The tx-store.path config value must be a string.",
491
+ exit_code=EXIT_USAGE,
492
+ )
493
+
494
+ resolved_path = tx_store_flag or config_path
495
+
496
+ edition = table.get("default-snomed-edition")
497
+ if edition is not None and not isinstance(edition, str):
498
+ raise CliError(
499
+ "The tx-store.default-snomed-edition config value must be a string.",
500
+ exit_code=EXIT_USAGE,
501
+ )
502
+
503
+ cache_size = table.get("expansion-cache-size")
504
+ if cache_size is not None:
505
+ # A bool is an int in Python; reject it so the value is an honest count.
506
+ if isinstance(cache_size, bool) or not isinstance(cache_size, int):
507
+ raise CliError(
508
+ "The tx-store.expansion-cache-size config value must be an integer.",
509
+ exit_code=EXIT_USAGE,
510
+ )
511
+ if cache_size < 1:
512
+ raise CliError(
513
+ "The tx-store.expansion-cache-size config value must be a "
514
+ "positive integer.",
515
+ exit_code=EXIT_USAGE,
516
+ )
517
+
518
+ dialect = table.get("default-dialect")
519
+ if dialect is not None and not isinstance(dialect, str):
520
+ raise CliError(
521
+ "The tx-store.default-dialect config value must be a string.",
522
+ exit_code=EXIT_USAGE,
523
+ )
524
+
525
+ aliases = table.get("dialect-aliases")
526
+ if aliases is not None and not _is_string_table(aliases):
527
+ # A malformed alias table is a mistake worth reporting, but it cannot make
528
+ # a lookup wrong, so it is warned about and dropped rather than fatal.
529
+ notify = on_warning or (lambda message: print(message, file=sys.stderr))
530
+ notify(
531
+ "Ignoring 'tx-store.dialect-aliases'; it must be a table mapping a "
532
+ "language tag to a SNOMED CT language reference set identifier, for "
533
+ 'example [tx-store.dialect-aliases] with en-NZ = "271000210107".'
534
+ )
535
+ aliases = None
536
+
537
+ # Without a path, tuning keys cannot activate local mode; warn that the
538
+ # table is inert rather than silently ignoring it (FR-009).
539
+ if resolved_path is None:
540
+ if table:
541
+ notify = on_warning or (lambda message: print(message, file=sys.stderr))
542
+ notify(
543
+ "The [tx-store] config table has no effect without a 'path' key "
544
+ "(or the --tx-store flag); remote terminology mode will be used."
545
+ )
546
+ return None
547
+
548
+ return TxStore(
549
+ path=resolved_path,
550
+ default_snomed_edition=edition,
551
+ expansion_cache_size=cache_size,
552
+ dialect_aliases=aliases,
553
+ default_dialect=dialect,
554
+ )
555
+
556
+
557
+ def _is_string_table(value: object) -> bool:
558
+ """Reports whether a config value is a table whose keys and values are strings.
559
+
560
+ :param value: the value read from the config file.
561
+ :return: True when the value is a dict of string to string.
562
+ """
563
+ return isinstance(value, dict) and all(
564
+ isinstance(key, str) and isinstance(entry, str) for key, entry in value.items()
565
+ )
566
+
567
+
388
568
  def resolve_config_source(
389
569
  config_path: Optional[Path],
390
570
  cwd: Path,
@@ -425,6 +605,7 @@ def resolve_config(
425
605
  config_path: Optional[Path] = None,
426
606
  cwd: Optional[Path] = None,
427
607
  env: Optional[dict] = None,
608
+ tx_store: Optional[str] = None,
428
609
  on_warning: Optional[Callable[[str], None]] = None,
429
610
  on_notice: Optional[Callable[[str], None]] = None,
430
611
  ) -> CliConfig:
@@ -449,6 +630,8 @@ def resolve_config(
449
630
  :param cwd: the directory searched for a project-local ``pathling.toml``;
450
631
  defaults to the current working directory.
451
632
  :param env: the environment mapping for secret resolution.
633
+ :param tx_store: the ``--tx-store`` flag value selecting a local terminology
634
+ store, or None.
452
635
  :param on_warning: an optional warning callback passed to the file loader and
453
636
  used to surface the managed Spark-package version-override warning;
454
637
  defaults to writing to stderr so warnings appear even in quiet mode.
@@ -485,6 +668,10 @@ def resolve_config(
485
668
  else:
486
669
  on_notice(f"Using project config {path}.")
487
670
 
671
+ # Record whether the server URL was set explicitly (flag or config key) as
672
+ # opposed to falling back to the built-in default; the store-wins conflict
673
+ # warning fires only for an explicit server (FR-004).
674
+ tx_server_explicit = bool(tx_server or file_data.get("tx-server"))
488
675
  resolved_tx_server = tx_server or file_data.get("tx-server") or DEFAULT_TX_SERVER
489
676
  resolved_fhir_version = (
490
677
  fhir_version or file_data.get("fhir-version") or DEFAULT_FHIR_VERSION
@@ -508,10 +695,15 @@ def resolve_config(
508
695
  env,
509
696
  )
510
697
 
698
+ # Resolve the local terminology store; its presence selects local mode.
699
+ resolved_tx_store = _resolve_tx_store(file_data, tx_store, on_warning)
700
+
511
701
  # Some terminology auth input was supplied but it is insufficient to
512
702
  # authenticate (a client ID and a token endpoint are both required); tell the
513
- # user rather than silently disabling it (FR-005).
514
- if tx_auth is not None and not tx_auth.enabled:
703
+ # user rather than silently disabling it (FR-005). In local mode the store
704
+ # wins and auth is ignored entirely, so this incomplete-auth warning is
705
+ # suppressed in favour of the store-wins warning below.
706
+ if resolved_tx_store is None and tx_auth is not None and not tx_auth.enabled:
515
707
  notify = on_warning or (lambda message: print(message, file=sys.stderr))
516
708
  notify(
517
709
  "Terminology authentication is incomplete and will be disabled: a "
@@ -531,6 +723,24 @@ def resolve_config(
531
723
  resolved_spark = resolve_spark_conf(spark_table, spark_conf_flags, env)
532
724
  spark_conf = merge_spark_conf(resolved_spark, on_warning=spark_warn)
533
725
 
726
+ # When a store is selected it wins over any server or authentication
727
+ # settings; warn about the ones that were explicitly configured so the
728
+ # override is never a silent surprise (FR-004, FR-005). An explicitly set
729
+ # authentication is also disabled so it can never reach the session.
730
+ if resolved_tx_store is not None:
731
+ conflict_warn = on_warning or (lambda message: print(message, file=sys.stderr))
732
+ if tx_server_explicit:
733
+ conflict_warn(
734
+ "A local terminology store is configured, so the terminology "
735
+ "server setting is ignored."
736
+ )
737
+ if tx_auth is not None:
738
+ conflict_warn(
739
+ "A local terminology store is configured, so the terminology "
740
+ "authentication settings are ignored."
741
+ )
742
+ tx_auth = None
743
+
534
744
  return CliConfig(
535
745
  tx_server=resolved_tx_server,
536
746
  tx_auth=tx_auth,
@@ -539,4 +749,6 @@ def resolve_config(
539
749
  config_path=path if path.exists() else None,
540
750
  spark_conf=spark_conf,
541
751
  bulk_auth_table=file_data.get("bulk-auth"),
752
+ tx_store=resolved_tx_store,
753
+ tx_server_explicit=tx_server_explicit,
542
754
  )
@@ -0,0 +1,115 @@
1
+ #
2
+ # Copyright © 2018-2026 Commonwealth Scientific and Industrial Research
3
+ # Organisation (CSIRO) ABN 41 687 119 230.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ """The ``pathling console`` command.
19
+
20
+ Opens an interactive IPython session with ``spark`` (the Spark session), ``pc``
21
+ (the configured Pathling context), and the Pathling package's public API
22
+ pre-imported into the user namespace, after a banner identifying the version and
23
+ the variables in scope. The ``pathling`` name is bound to the package module
24
+ itself, so typing ``import pathling`` at the prompt cannot clobber the context.
25
+ The terminology display function is bound as ``tx_display`` rather than
26
+ ``display`` so that IPython's built-in ``display`` remains reachable at the
27
+ prompt. IPython is imported inside the command body so that ``--help`` stays
28
+ fast.
29
+
30
+ Author: John Grimes.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import platform
36
+ from typing import TYPE_CHECKING
37
+
38
+ import click
39
+
40
+ from pathling._version import __version__
41
+ from pathling.cli import session
42
+
43
+ if TYPE_CHECKING:
44
+ from pathling.cli.main import CliContext
45
+
46
+
47
+ def build_banner() -> str:
48
+ """Builds the banner shown before the console's first prompt.
49
+
50
+ The banner identifies the Pathling and Python versions, lists the
51
+ variables in scope - naming the context ``pc``, the name a user copies into
52
+ their next command - notes that the Pathling public functions (e.g.
53
+ ``member_of``, ``translate``, ``to_coding``) are pre-imported, points to
54
+ the Python API reference for the full list, notes that the terminology
55
+ display is available as ``tx_display`` rather than ``display``, and
56
+ explains how to exit.
57
+
58
+ :return: the banner text.
59
+ """
60
+ return (
61
+ f"Pathling console (version {__version__}, "
62
+ f"Python {platform.python_version()})\n"
63
+ "Variables in scope: spark (SparkSession), pc (PathlingContext)\n"
64
+ "Pathling public functions are pre-imported (member_of, translate, "
65
+ "to_coding, ...).\n"
66
+ "See https://pathling.csiro.au/docs/python/pathling.html#module-pathling "
67
+ "for the full list.\n"
68
+ "The terminology display function is available as tx_display "
69
+ "(display is IPython's built-in).\n"
70
+ "Type exit or press Ctrl-D to leave.\n"
71
+ )
72
+
73
+
74
+ @click.command(name="console")
75
+ @click.pass_obj
76
+ def console(obj: CliContext) -> None:
77
+ """Open an interactive console with the Pathling environment ready.
78
+
79
+ Starts an IPython session with spark (the Spark session) and pc
80
+ (the configured Pathling context) in scope. The Pathling public functions
81
+ (member_of, translate, to_coding, and so on) are pre-imported, so no
82
+ "from pathling import ..." is needed; the terminology display is available
83
+ as tx_display, leaving IPython's built-in display unchanged. Exit with
84
+ 'exit' or Ctrl-D.
85
+
86
+ \b
87
+ See the Pathling Python API reference:
88
+ https://pathling.csiro.au/docs/python/pathling.html
89
+
90
+ Examples:
91
+
92
+ pathling console
93
+
94
+ pathling --tx-server https://tx.example.org/fhir console
95
+ """
96
+ pc = session.create_context(obj.config, obj.console)
97
+
98
+ # Pre-import the public API surface, but expose the terminology display as
99
+ # tx_display only: IPython installs its own `display` into the interpreter's
100
+ # built-ins, so binding Pathling's display here would silently shadow it.
101
+ user_ns = session.public_namespace()
102
+ user_ns["tx_display"] = user_ns.pop("display")
103
+ user_ns["spark"] = pc.spark
104
+ user_ns["pc"] = pc
105
+
106
+ import IPython
107
+ from traitlets.config import Config
108
+
109
+ config = Config()
110
+ config.TerminalInteractiveShell.banner1 = build_banner()
111
+ IPython.start_ipython(
112
+ argv=[],
113
+ user_ns=user_ns,
114
+ config=config,
115
+ )
@@ -24,9 +24,13 @@ counts written.
24
24
  Author: John Grimes.
25
25
  """
26
26
 
27
+ from __future__ import annotations
28
+
27
29
  from pathlib import Path
30
+ from typing import TYPE_CHECKING, Optional, Tuple
28
31
 
29
32
  import click
33
+ from rich.console import Console
30
34
  from rich.table import Table
31
35
 
32
36
  from pathling.cli import session
@@ -34,6 +38,10 @@ from pathling.cli.errors import EXIT_USAGE, CliError
34
38
  from pathling.cli.io import FROM_CHOICES, read_source, resolve_source
35
39
  from pathling.cli.render import check_overwrite, progress_status
36
40
 
41
+ if TYPE_CHECKING:
42
+ from pathling.cli.main import CliContext
43
+ from pathling.datasource import DataSource
44
+
37
45
  # The output formats convert can write.
38
46
  TO_CHOICES = ("ndjson", "parquet", "delta")
39
47
 
@@ -83,7 +91,16 @@ MODE_CHOICES = ("overwrite", "error", "append", "merge")
83
91
  help="Replace an existing output path (equivalent to --mode overwrite).",
84
92
  )
85
93
  @click.pass_obj
86
- def convert(obj, source, from_format, to_format, output, mode, types, overwrite):
94
+ def convert(
95
+ obj: CliContext,
96
+ source: str,
97
+ from_format: Optional[str],
98
+ to_format: str,
99
+ output: str,
100
+ mode: str,
101
+ types: Tuple[str, ...],
102
+ overwrite: bool,
103
+ ) -> None:
87
104
  """Convert FHIR data between formats.
88
105
 
89
106
  Examples:
@@ -134,7 +151,9 @@ def convert(obj, source, from_format, to_format, output, mode, types, overwrite)
134
151
  _print_summary(console, data_source, output_path)
135
152
 
136
153
 
137
- def _print_summary(console, data_source, output_path) -> None:
154
+ def _print_summary(
155
+ console: Console, data_source: DataSource, output_path: Path
156
+ ) -> None:
138
157
  """Prints a table of the resource types written and the output location.
139
158
 
140
159
  Row counts are deliberately omitted: counting each type would trigger an
@@ -28,15 +28,22 @@ keeping the move on a single filesystem.
28
28
  Author: John Grimes.
29
29
  """
30
30
 
31
+ from __future__ import annotations
32
+
31
33
  from pathlib import Path
32
- from typing import Union
34
+ from typing import TYPE_CHECKING, Tuple, Union
33
35
 
34
36
  from py4j.java_gateway import is_instance_of
35
37
 
36
38
  from pathling.cli.errors import CliError
37
39
 
40
+ if TYPE_CHECKING:
41
+ from pyspark.sql import SparkSession
42
+
38
43
 
39
- def _filesystem(spark, reference_path: Union[str, Path]):
44
+ def _filesystem(
45
+ spark: SparkSession, reference_path: Union[str, Path]
46
+ ) -> Tuple[object, object]:
40
47
  """Resolves the Hadoop ``FileSystem`` for a path, unwrapping checksums.
41
48
 
42
49
  The filesystem is resolved from ``reference_path`` so that operations stay
@@ -63,7 +70,7 @@ def _filesystem(spark, reference_path: Union[str, Path]):
63
70
  return fs, path_class
64
71
 
65
72
 
66
- def remove_path(spark, path: Union[str, Path]) -> None:
73
+ def remove_path(spark: SparkSession, path: Union[str, Path]) -> None:
67
74
  """Removes a file or directory over its Hadoop ``FileSystem``, if it exists.
68
75
 
69
76
  Used to clear an existing target before an overwrite and to clean up the
@@ -81,7 +88,7 @@ def remove_path(spark, path: Union[str, Path]) -> None:
81
88
 
82
89
 
83
90
  def departition(
84
- spark,
91
+ spark: SparkSession,
85
92
  source_dir: Union[str, Path],
86
93
  target_path: Union[str, Path],
87
94
  part_extension: str,