pathling 9.9.0.dev0__tar.gz → 9.10.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 (53) hide show
  1. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/PKG-INFO +1 -1
  2. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/_version.py +2 -2
  3. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/config.py +15 -1
  4. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/import_terminology.py +147 -6
  5. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/context.py +33 -5
  6. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/.gitignore +0 -0
  7. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/LICENSE +0 -0
  8. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/README.md +0 -0
  9. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/bulk.py +0 -0
  10. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/data/bundles/Bennett146_Swaniawski813_704c9750-f6e6-473b-ee83-fbd48e07fe3f.json +0 -0
  11. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/data/bundles/Dino214_Parisian75_40d82b80-b682-cd8b-da6d-396809878641.json +0 -0
  12. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/data/csv/conditions.csv +0 -0
  13. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/data/resources/Condition.ndjson +0 -0
  14. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/data/resources/Patient.ndjson +0 -0
  15. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/designation.py +0 -0
  16. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/display.py +0 -0
  17. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/encode_bundles.py +0 -0
  18. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/encode_resources.py +0 -0
  19. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/fhir_search.py +0 -0
  20. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/fhir_view.py +0 -0
  21. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/member_of.py +0 -0
  22. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/property_of.py +0 -0
  23. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/subsumes.py +0 -0
  24. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/examples/translate.py +0 -0
  25. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/__init__.py +0 -0
  26. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/_spark_defaults.py +0 -0
  27. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/bulk.py +0 -0
  28. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/__init__.py +0 -0
  29. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/console.py +0 -0
  30. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/convert.py +0 -0
  31. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/departition.py +0 -0
  32. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/errors.py +0 -0
  33. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/export.py +0 -0
  34. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/fhirpath.py +0 -0
  35. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/io.py +0 -0
  36. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/main.py +0 -0
  37. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/render.py +0 -0
  38. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/resources/quiet-log4j2.properties +0 -0
  39. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/ruff.toml +0 -0
  40. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/run.py +0 -0
  41. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/session.py +0 -0
  42. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/sparkconf.py +0 -0
  43. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/terminology.py +0 -0
  44. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/cli/view.py +0 -0
  45. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/coding.py +0 -0
  46. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/core.py +0 -0
  47. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/datasink.py +0 -0
  48. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/datasource.py +0 -0
  49. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/fhir.py +0 -0
  50. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/functions.py +0 -0
  51. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/spark.py +0 -0
  52. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pathling/udfs.py +0 -0
  53. {pathling-9.9.0.dev0 → pathling-9.10.0.dev0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pathling
3
- Version: 9.9.0.dev0
3
+ Version: 9.10.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>
@@ -2,8 +2,8 @@
2
2
  # Auto generated from POM project version.
3
3
  # Please do not modify.
4
4
  #
5
- __version__="9.9.0.dev0"
6
- __java_version__="9.9.0-SNAPSHOT"
5
+ __version__="9.10.0.dev0"
6
+ __java_version__="9.10.0-SNAPSHOT"
7
7
  __scala_version__="2.13"
8
8
  __delta_version__="4.0.0"
9
9
  __hadoop_version__="3.4.1"
@@ -55,7 +55,15 @@ PROJECT_CONFIG_FILENAME = "pathling.toml"
55
55
 
56
56
  # Valid top-level keys in the config file.
57
57
  VALID_CONFIG_KEYS = frozenset(
58
- {"tx-server", "fhir-version", "terminology-auth", "bulk-auth", "spark", "tx-store"}
58
+ {
59
+ "tx-server",
60
+ "fhir-version",
61
+ "terminology-auth",
62
+ "bulk-auth",
63
+ "spark",
64
+ "tx-store",
65
+ "package-registry",
66
+ }
59
67
  )
60
68
 
61
69
  # Valid keys within the [terminology-auth] and [bulk-auth] tables.
@@ -181,6 +189,10 @@ class CliConfig:
181
189
  :param tx_server_explicit: whether the terminology server URL was set
182
190
  explicitly (via flag or config key) rather than falling back to the
183
191
  built-in default. Drives the store-wins conflict warning.
192
+ :param package_registry: the FHIR package registry that a package import
193
+ checks a package against, or None to leave the choice to the library
194
+ default. Concerns package distribution rather than the store, so it
195
+ is a top-level key rather than part of ``[tx-store]``.
184
196
  """
185
197
 
186
198
  tx_server: str = DEFAULT_TX_SERVER
@@ -192,6 +204,7 @@ class CliConfig:
192
204
  bulk_auth_table: Optional[dict] = None
193
205
  tx_store: Optional[TxStore] = None
194
206
  tx_server_explicit: bool = False
207
+ package_registry: Optional[str] = None
195
208
 
196
209
 
197
210
  def _load_toml(path: Path) -> dict:
@@ -751,4 +764,5 @@ def resolve_config(
751
764
  bulk_auth_table=file_data.get("bulk-auth"),
752
765
  tx_store=resolved_tx_store,
753
766
  tx_server_explicit=tx_server_explicit,
767
+ package_registry=file_data.get("package-registry"),
754
768
  )
@@ -35,13 +35,26 @@ from rich.console import Console
35
35
 
36
36
  from pathling.cli import session
37
37
  from pathling.cli.config import CliConfig
38
- from pathling.cli.errors import EXIT_USAGE, CliError
38
+ from pathling.cli.errors import EXIT_USAGE, CliError, unwrap_java_exception
39
39
  from pathling.cli.render import progress_status
40
40
 
41
41
  if TYPE_CHECKING:
42
+ from pyspark.sql import Row, SparkSession
43
+
42
44
  from pathling import PathlingContext
43
45
  from pathling.cli.main import CliContext
44
46
 
47
+ # The name of the manifest table within a terminology store.
48
+ MANIFEST_TABLE = "manifest"
49
+
50
+ # The fixed phrase the library uses when a package does not match the checksum
51
+ # its registry publishes. The library must not name a CLI flag, so the hint
52
+ # naming --no-verify is added here, keyed off this phrase.
53
+ MISMATCH_PHRASE = "does not match the registry checksum"
54
+
55
+ # The hint appended to a checksum mismatch failure.
56
+ MISMATCH_HINT = "Re-run with --no-verify to import it anyway."
57
+
45
58
 
46
59
  def _import_context(config: CliConfig, console: Console) -> PathlingContext:
47
60
  """Creates a context for importing into a store.
@@ -84,6 +97,91 @@ def _resolve_storage_path(config: CliConfig, storage_path: Optional[str]) -> str
84
97
  )
85
98
 
86
99
 
100
+ def read_latest_import_row(
101
+ spark: SparkSession, store: str, source: str
102
+ ) -> Optional[Row]:
103
+ """Reads the most recent manifest row recorded for a source.
104
+
105
+ The store manifest carries the provenance of each import: the source hash,
106
+ the package identity, and the outcome of the registry check. The latest row
107
+ for this source describes the import that has just completed.
108
+
109
+ A failure to read the manifest yields None rather than an error: the import
110
+ has already committed, and reporting less detail is better than failing a
111
+ command whose work succeeded.
112
+
113
+ :param spark: the Spark session to read with.
114
+ :param store: the terminology store path.
115
+ :param source: the import source, matched against the manifest ``source``
116
+ column.
117
+ :return: the latest manifest row for the source, or None when there is none
118
+ or the manifest cannot be read.
119
+ """
120
+ from pyspark.sql import functions as F
121
+
122
+ manifest_path = f"{store.rstrip('/')}/{MANIFEST_TABLE}"
123
+ try:
124
+ rows = (
125
+ spark.read.format("delta")
126
+ .load(manifest_path)
127
+ .filter(F.col("source") == source)
128
+ .orderBy(F.col("imported_at").desc())
129
+ .take(1)
130
+ )
131
+ except Exception: # noqa: BLE001 - detail only; the import already committed.
132
+ return None
133
+ return rows[0] if rows else None
134
+
135
+
136
+ def format_import_summary(
137
+ command_noun: str, source: str, store: str, row: Optional[Row]
138
+ ) -> str:
139
+ """Renders the completion line for a finished import.
140
+
141
+ The line always names what was imported, from where, and into where. Where
142
+ the manifest records provenance it is reported in parentheses: the source
143
+ hash, and for a package the identity and the outcome of the registry check.
144
+
145
+ :param command_noun: what was imported, such as ``"SNOMED CT"``.
146
+ :param source: the import source as given on the command line.
147
+ :param store: the resolved store path.
148
+ :param row: the manifest row describing the import, or None when it could
149
+ not be read.
150
+ :return: the completion line.
151
+ """
152
+ summary = f"Imported {command_noun} from {source} into {store}"
153
+ if row is None:
154
+ return summary
155
+ values = row.asDict() if hasattr(row, "asDict") else dict(row)
156
+ sha256 = values.get("source_sha256")
157
+ if sha256 is None:
158
+ # A directory source has no single set of bytes to fingerprint.
159
+ return summary
160
+ status = values.get("package_verification")
161
+ name = values.get("package_name")
162
+ version = values.get("package_version")
163
+ identity = f"{name} {version}" if name and version else None
164
+ if status == "verified":
165
+ registry = values.get("package_registry")
166
+ detail = f"verified {identity} against {registry}"
167
+ elif status == "unverified":
168
+ subject = identity if identity else "package"
169
+ detail = (
170
+ f"{subject} not verified against a registry; re-run with --verbose "
171
+ "for the reason"
172
+ )
173
+ elif status == "skipped":
174
+ detail = (
175
+ f"{identity}, verification skipped"
176
+ if identity
177
+ else "package verification skipped"
178
+ )
179
+ else:
180
+ # Not a package: the hash alone is the provenance.
181
+ return f"{summary} (sha256 {sha256})"
182
+ return f"{summary} ({detail}; sha256 {sha256})"
183
+
184
+
87
185
  @click.command(name="import-snomed")
88
186
  @click.argument("source")
89
187
  @click.argument("storage_path", required=False)
@@ -142,20 +240,46 @@ def import_snomed(
142
240
  pc.import_snomed(
143
241
  source, resolved_path, edition_uri, dense_id_order, resolved_dialect
144
242
  )
145
- click.echo(f"Imported SNOMED CT from {source} into {resolved_path}")
243
+ row = read_latest_import_row(pc.spark, resolved_path, source)
244
+ click.echo(format_import_summary("SNOMED CT", source, resolved_path, row))
146
245
 
147
246
 
148
247
  @click.command(name="import-fhir-terminology")
149
248
  @click.argument("source")
150
249
  @click.argument("storage_path", required=False)
250
+ @click.option(
251
+ "--no-verify",
252
+ "no_verify",
253
+ is_flag=True,
254
+ help=(
255
+ "Do not check a FHIR NPM package against its registry's published "
256
+ "checksum. Use for offline imports or packages not published to a "
257
+ "registry."
258
+ ),
259
+ )
260
+ @click.option(
261
+ "--package-registry",
262
+ "package_registry",
263
+ metavar="URL",
264
+ help=(
265
+ "The FHIR package registry to check a package against. Falls back to "
266
+ "the 'package-registry' config key, then https://packages.fhir.org."
267
+ ),
268
+ )
151
269
  @click.pass_obj
152
270
  def import_fhir_terminology(
153
- obj: CliContext, source: str, storage_path: Optional[str]
271
+ obj: CliContext,
272
+ source: str,
273
+ storage_path: Optional[str],
274
+ no_verify: bool,
275
+ package_registry: Optional[str],
154
276
  ) -> None:
155
277
  """Import FHIR CodeSystem, ValueSet, and ConceptMap resources into a store.
156
278
 
157
279
  The source may be a JSON file, a directory of JSON files, or a FHIR NPM
158
- package (.tgz). STORAGE_PATH may be omitted when 'tx-store.path' (or
280
+ package (.tgz). A package is checked against the checksum its registry
281
+ publishes; a package that does not match fails the import and leaves the
282
+ store unchanged. STORAGE_PATH may be omitted when 'tx-store.path' (or
159
283
  --tx-store) is set.
160
284
 
161
285
  Example:
@@ -165,10 +289,27 @@ def import_fhir_terminology(
165
289
  config = obj.config
166
290
  console = obj.console
167
291
  resolved_path = _resolve_storage_path(config, storage_path)
292
+ # The flag wins over the config key; with neither set no registry is named
293
+ # and the library default applies, so the CLI never holds that URL.
294
+ resolved_registry = (
295
+ package_registry if package_registry is not None else config.package_registry
296
+ )
168
297
  pc = _import_context(config, console)
169
298
  with progress_status(console, "Importing FHIR terminology...", config.verbose):
170
- pc.import_fhir_terminology(source, resolved_path)
171
- click.echo(f"Imported FHIR terminology from {source} into {resolved_path}")
299
+ try:
300
+ pc.import_fhir_terminology(
301
+ source,
302
+ resolved_path,
303
+ verify_package=not no_verify,
304
+ package_registry=resolved_registry,
305
+ )
306
+ except Exception as exc: # noqa: BLE001 - enrich a mismatch failure.
307
+ root_message = unwrap_java_exception(exc)
308
+ if MISMATCH_PHRASE not in root_message:
309
+ raise
310
+ raise CliError(f"{root_message} {MISMATCH_HINT}") from exc
311
+ row = read_latest_import_row(pc.spark, resolved_path, source)
312
+ click.echo(format_import_summary("FHIR terminology", source, resolved_path, row))
172
313
 
173
314
 
174
315
  #: The terminology import commands registered by the root command group.
@@ -254,8 +254,8 @@ class PathlingContext:
254
254
  unversioned SNOMED reference when the local store holds multiple editions.
255
255
  :param expansion_cache_size: the maximum number of value set expansions cached per executor
256
256
  in local mode.
257
- :param dialect_aliases: additional dialect tags recognised in local mode when a display or
258
- designation is requested in a particular language, mapping a language tag to the
257
+ :param dialect_aliases: additional dialect tags recognised in local mode when a display
258
+ is requested in a particular language, mapping a language tag to the
259
259
  identifier of the SNOMED CT language reference set that serves it (for example
260
260
  ``{"en-NZ": "271000210107"}``). An entry for a tag that is already recognised
261
261
  replaces the built-in mapping for it.
@@ -473,18 +473,46 @@ class PathlingContext:
473
473
  options = builder.build()
474
474
  self._jpc.importSnomed(source, storage_path, options)
475
475
 
476
- def import_fhir_terminology(self, source: str, storage_path: str) -> None:
476
+ def import_fhir_terminology(
477
+ self,
478
+ source: str,
479
+ storage_path: str,
480
+ verify_package: bool = True,
481
+ package_registry: Optional[str] = None,
482
+ ) -> None:
477
483
  """
478
484
  Imports FHIR R4 CodeSystem, ValueSet, and ConceptMap resources into a local terminology
479
485
  store.
480
486
 
487
+ A FHIR NPM package (``.tgz``) is checked against the checksum its registry publishes before
488
+ anything is written: a package that does not match fails the import and leaves the store
489
+ untouched. A package the registry does not describe, or a registry that cannot be reached,
490
+ is imported and recorded as unverified. For an offline import, or a package that was never
491
+ published to a registry, pass ``verify_package=False``; the import is then recorded as
492
+ having skipped verification.
493
+
481
494
  :param source: the path to a JSON file, a directory of JSON files, or a FHIR NPM package
482
495
  (``.tgz``), on any filesystem accessible through the Hadoop FileSystem API
483
496
  :param storage_path: the terminology store location, created if absent
497
+ :param verify_package: whether a package source is checked against its registry's published
498
+ checksum; ``True`` by default
499
+ :param package_registry: the FHIR package registry to check a package against; when omitted
500
+ the library default (``https://packages.fhir.org``) is used
484
501
  :raises: the mapped JVM ``TerminologyImportException`` if the source contains no importable
485
- resources or an invalid resource; the store is left unmodified
502
+ resources or an invalid resource, or the package does not match the registry
503
+ checksum; the store is left unmodified
486
504
  """
487
- self._jpc.importFhirTerminology(source, storage_path)
505
+ options = None
506
+ if not verify_package or package_registry is not None:
507
+ jvm = self._spark._jvm
508
+ builder = (
509
+ jvm.au.csiro.pathling.library.terminology.FhirImportOptions.builder()
510
+ )
511
+ builder = builder.verifyPackage(verify_package)
512
+ if package_registry is not None:
513
+ builder = builder.packageRegistry(package_registry)
514
+ options = builder.build()
515
+ self._jpc.importFhirTerminology(source, storage_path, options)
488
516
 
489
517
  def encode(
490
518
  self,
File without changes
File without changes
File without changes