echr-py 0.2.2__tar.gz → 0.2.3__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 (141) hide show
  1. {echr_py-0.2.2 → echr_py-0.2.3}/CITATION.cff +1 -1
  2. {echr_py-0.2.2/echr_py.egg-info → echr_py-0.2.3}/PKG-INFO +18 -7
  3. {echr_py-0.2.2 → echr_py-0.2.3}/README.md +17 -6
  4. {echr_py-0.2.2 → echr_py-0.2.3/echr_py.egg-info}/PKG-INFO +18 -7
  5. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/__init__.py +1 -1
  6. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/bilingual/__init__.py +2 -0
  7. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/bilingual/rescue.py +62 -2
  8. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/opinions.py +62 -7
  9. {echr_py-0.2.2 → echr_py-0.2.3}/pyproject.toml +1 -1
  10. {echr_py-0.2.2 → echr_py-0.2.3}/LICENSE +0 -0
  11. {echr_py-0.2.2 → echr_py-0.2.3}/MANIFEST.in +0 -0
  12. {echr_py-0.2.2 → echr_py-0.2.3}/RIGHTS.md +0 -0
  13. {echr_py-0.2.2 → echr_py-0.2.3}/SECURITY.md +0 -0
  14. {echr_py-0.2.2 → echr_py-0.2.3}/echr_py.egg-info/SOURCES.txt +0 -0
  15. {echr_py-0.2.2 → echr_py-0.2.3}/echr_py.egg-info/dependency_links.txt +0 -0
  16. {echr_py-0.2.2 → echr_py-0.2.3}/echr_py.egg-info/entry_points.txt +0 -0
  17. {echr_py-0.2.2 → echr_py-0.2.3}/echr_py.egg-info/requires.txt +0 -0
  18. {echr_py-0.2.2 → echr_py-0.2.3}/echr_py.egg-info/top_level.txt +0 -0
  19. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/_aio.py +0 -0
  20. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/_sync.py +0 -0
  21. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/bilingual/bundle.py +0 -0
  22. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/bilingual/corpus.py +0 -0
  23. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/bilingual/ecli.py +0 -0
  24. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/bilingual/reconcile.py +0 -0
  25. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/cache/__init__.py +0 -0
  26. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/cache/store.py +0 -0
  27. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/__init__.py +0 -0
  28. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/artifacts.py +0 -0
  29. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/authority.py +0 -0
  30. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/benchmarks.py +0 -0
  31. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/catalog.py +0 -0
  32. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/extractor.py +0 -0
  33. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/graph.py +0 -0
  34. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/metrics.py +0 -0
  35. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/models.py +0 -0
  36. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/occurrences.py +0 -0
  37. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/paragraphs.py +0 -0
  38. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/reporter.py +0 -0
  39. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/citations/resolver.py +0 -0
  40. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/cli.py +0 -0
  41. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/config.py +0 -0
  42. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/__init__.py +0 -0
  43. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/citation_authority.csv +0 -0
  44. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/citation_authority.json +0 -0
  45. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/citation_authority_eng.csv +0 -0
  46. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/citation_authority_eng.json +0 -0
  47. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/citation_authority_supplements.json +0 -0
  48. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/historical_citation_catalog.json +0 -0
  49. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/data/kpthesaurus_eng.json +0 -0
  50. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/__init__.py +0 -0
  51. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/_sync.py +0 -0
  52. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/aio.py +0 -0
  53. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/bridge.py +0 -0
  54. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/client.py +0 -0
  55. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/collections.py +0 -0
  56. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/downloader.py +0 -0
  57. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/imports.py +0 -0
  58. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/queries.py +0 -0
  59. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/raw_download.py +0 -0
  60. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/related_scraper.py +0 -0
  61. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/execution/textract.py +0 -0
  62. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/extractors/__init__.py +0 -0
  63. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/extractors/base.py +0 -0
  64. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/__init__.py +0 -0
  65. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/adapters.py +0 -0
  66. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/assets/LICENSE-D3.txt +0 -0
  67. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/assets/d3.v7.min.js +0 -0
  68. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/export.py +0 -0
  69. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/models.py +0 -0
  70. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/graphs/template.py +0 -0
  71. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/gui/__init__.py +0 -0
  72. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/gui/app.py +0 -0
  73. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/llm/__init__.py +0 -0
  74. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/llm/base.py +0 -0
  75. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/llm/batch.py +0 -0
  76. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/llm/client.py +0 -0
  77. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/llm/providers.py +0 -0
  78. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/__init__.py +0 -0
  79. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/browse.py +0 -0
  80. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/codebook.py +0 -0
  81. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/export.py +0 -0
  82. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/paragraphs.py +0 -0
  83. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/registry.py +0 -0
  84. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/local/search.py +0 -0
  85. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/main/__init__.py +0 -0
  86. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/main/client.py +0 -0
  87. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/main/downloader.py +0 -0
  88. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/main/dsl.py +0 -0
  89. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/main/queries.py +0 -0
  90. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/main/url.py +0 -0
  91. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/mcp/__init__.py +0 -0
  92. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/mcp/__main__.py +0 -0
  93. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/mcp/install.py +0 -0
  94. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/mcp/server.py +0 -0
  95. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/__init__.py +0 -0
  96. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/case.py +0 -0
  97. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/citation.py +0 -0
  98. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/common.py +0 -0
  99. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/document.py +0 -0
  100. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/exec_case.py +0 -0
  101. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/exec_document.py +0 -0
  102. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/models/version.py +0 -0
  103. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/pipeline/__init__.py +0 -0
  104. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/pipeline/runner.py +0 -0
  105. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/pipeline/validation.py +0 -0
  106. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/retrieval/__init__.py +0 -0
  107. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/retrieval/benchmark.py +0 -0
  108. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/retrieval/embeddings.py +0 -0
  109. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/retrieval/hybrid.py +0 -0
  110. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/retrieval/providers.py +0 -0
  111. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/__init__.py +0 -0
  112. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/batch.py +0 -0
  113. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/hooks.py +0 -0
  114. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/jobs.py +0 -0
  115. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/models.py +0 -0
  116. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/runner.py +0 -0
  117. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/schemas/citation-use-minimal.json +0 -0
  118. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/schemas/citation-use-multiaxial.json +0 -0
  119. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/spec.py +0 -0
  120. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/studies/taxonomies.py +0 -0
  121. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/__init__.py +0 -0
  122. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/composition.py +0 -0
  123. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/conversion.py +0 -0
  124. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/dispositive.py +0 -0
  125. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/judges.py +0 -0
  126. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/segmentation.py +0 -0
  127. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/text/spine.py +0 -0
  128. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/thesaurus.py +0 -0
  129. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/utils/__init__.py +0 -0
  130. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/utils/downloads.py +0 -0
  131. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/utils/jsonl.py +0 -0
  132. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/utils/retry.py +0 -0
  133. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/utils/runlog.py +0 -0
  134. {echr_py-0.2.2 → echr_py-0.2.3}/hudoc_py/versions.py +0 -0
  135. {echr_py-0.2.2 → echr_py-0.2.3}/release-requirements.txt +0 -0
  136. {echr_py-0.2.2 → echr_py-0.2.3}/scripts/check_sdist.py +0 -0
  137. {echr_py-0.2.2 → echr_py-0.2.3}/scripts/check_wheel.py +0 -0
  138. {echr_py-0.2.2 → echr_py-0.2.3}/scripts/public_package_files.txt +0 -0
  139. {echr_py-0.2.2 → echr_py-0.2.3}/scripts/public_payload_hashes.json +0 -0
  140. {echr_py-0.2.2 → echr_py-0.2.3}/scripts/verify_release.py +0 -0
  141. {echr_py-0.2.2 → echr_py-0.2.3}/setup.cfg +0 -0
@@ -5,7 +5,7 @@ type: software
5
5
  authors:
6
6
  - family-names: Ridi
7
7
  given-names: Niccolo
8
- version: 0.2.2
8
+ version: 0.2.3
9
9
  date-released: 2026-08-21
10
10
  license: MIT
11
11
  repository-code: "https://github.com/niccoloridi/echr-py"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echr-py
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Pythonic acquisition of HUDOC and HUDOC-EXEC metadata and source documents for ECtHR research.
5
5
  Author-email: Niccolo Ridi <niccoloridi@gmail.com>
6
6
  License-Expression: MIT
@@ -102,7 +102,7 @@ Dynamic: license-file
102
102
  <a href="https://github.com/niccoloridi/echr-py/actions/workflows/ci.yml"><img src="https://github.com/niccoloridi/echr-py/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
103
103
  <img src="https://img.shields.io/badge/python-3.11%E2%80%933.14-3776AB.svg" alt="Python 3.11–3.14">
104
104
  <a href="https://github.com/niccoloridi/echr-py/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT license"></a>
105
- <img src="https://img.shields.io/badge/version-0.2.2-blue.svg" alt="Version 0.2.2">
105
+ <img src="https://img.shields.io/badge/version-0.2.3-blue.svg" alt="Version 0.2.3">
106
106
  </p>
107
107
 
108
108
  `echr-py` is a research engineering toolkit for the case law of the European
@@ -112,10 +112,6 @@ Court of Human Rights. It connects public
112
112
  research artifacts through Python, a command-line interface, and the Model
113
113
  Context Protocol (MCP).
114
114
 
115
- The distribution and primary command are now named `echr-py`. The established
116
- `hudoc_py` Python import and the legacy `hudoc-py` command remain available for
117
- backward compatibility.
118
-
119
115
  It is designed for work where provenance matters. A result can retain the exact
120
116
  HUDOC document, language version, section, paragraph, individual opinion,
121
117
  footnote, citation span, cited procedural document, cited-paragraph pinpoint,
@@ -495,7 +491,7 @@ without cloning the repository:
495
491
 
496
492
  ```bash
497
493
  claude mcp add --scope user --transport stdio echr-py -- \
498
- "$(command -v uvx)" --from 'echr-py[mcp]==0.2.2' echr-py mcp
494
+ "$(command -v uvx)" --from 'echr-py[mcp]==0.2.3' echr-py mcp
499
495
  claude mcp get echr-py
500
496
  ```
501
497
 
@@ -635,6 +631,21 @@ cd echr-py
635
631
  python -m pip install -e ".[all,dev]"
636
632
  ```
637
633
 
634
+ ### Names
635
+
636
+ The distribution and primary command are `echr-py`. The Python import is
637
+ `hudoc_py`, and the earlier `hudoc-py` command still works; both remain
638
+ available so existing code and scripts keep running.
639
+
640
+ ```python
641
+ import hudoc_py # import name
642
+ ```
643
+
644
+ ```bash
645
+ echr-py --version # primary command
646
+ hudoc-py --version # legacy alias, same program
647
+ ```
648
+
638
649
  ## Documentation
639
650
 
640
651
  - [Worked examples](https://github.com/niccoloridi/echr-py/blob/main/docs/examples.md)
@@ -14,7 +14,7 @@
14
14
  <a href="https://github.com/niccoloridi/echr-py/actions/workflows/ci.yml"><img src="https://github.com/niccoloridi/echr-py/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
15
15
  <img src="https://img.shields.io/badge/python-3.11%E2%80%933.14-3776AB.svg" alt="Python 3.11–3.14">
16
16
  <a href="https://github.com/niccoloridi/echr-py/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT license"></a>
17
- <img src="https://img.shields.io/badge/version-0.2.2-blue.svg" alt="Version 0.2.2">
17
+ <img src="https://img.shields.io/badge/version-0.2.3-blue.svg" alt="Version 0.2.3">
18
18
  </p>
19
19
 
20
20
  `echr-py` is a research engineering toolkit for the case law of the European
@@ -24,10 +24,6 @@ Court of Human Rights. It connects public
24
24
  research artifacts through Python, a command-line interface, and the Model
25
25
  Context Protocol (MCP).
26
26
 
27
- The distribution and primary command are now named `echr-py`. The established
28
- `hudoc_py` Python import and the legacy `hudoc-py` command remain available for
29
- backward compatibility.
30
-
31
27
  It is designed for work where provenance matters. A result can retain the exact
32
28
  HUDOC document, language version, section, paragraph, individual opinion,
33
29
  footnote, citation span, cited procedural document, cited-paragraph pinpoint,
@@ -407,7 +403,7 @@ without cloning the repository:
407
403
 
408
404
  ```bash
409
405
  claude mcp add --scope user --transport stdio echr-py -- \
410
- "$(command -v uvx)" --from 'echr-py[mcp]==0.2.2' echr-py mcp
406
+ "$(command -v uvx)" --from 'echr-py[mcp]==0.2.3' echr-py mcp
411
407
  claude mcp get echr-py
412
408
  ```
413
409
 
@@ -547,6 +543,21 @@ cd echr-py
547
543
  python -m pip install -e ".[all,dev]"
548
544
  ```
549
545
 
546
+ ### Names
547
+
548
+ The distribution and primary command are `echr-py`. The Python import is
549
+ `hudoc_py`, and the earlier `hudoc-py` command still works; both remain
550
+ available so existing code and scripts keep running.
551
+
552
+ ```python
553
+ import hudoc_py # import name
554
+ ```
555
+
556
+ ```bash
557
+ echr-py --version # primary command
558
+ hudoc-py --version # legacy alias, same program
559
+ ```
560
+
550
561
  ## Documentation
551
562
 
552
563
  - [Worked examples](https://github.com/niccoloridi/echr-py/blob/main/docs/examples.md)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: echr-py
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Pythonic acquisition of HUDOC and HUDOC-EXEC metadata and source documents for ECtHR research.
5
5
  Author-email: Niccolo Ridi <niccoloridi@gmail.com>
6
6
  License-Expression: MIT
@@ -102,7 +102,7 @@ Dynamic: license-file
102
102
  <a href="https://github.com/niccoloridi/echr-py/actions/workflows/ci.yml"><img src="https://github.com/niccoloridi/echr-py/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
103
103
  <img src="https://img.shields.io/badge/python-3.11%E2%80%933.14-3776AB.svg" alt="Python 3.11–3.14">
104
104
  <a href="https://github.com/niccoloridi/echr-py/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT license"></a>
105
- <img src="https://img.shields.io/badge/version-0.2.2-blue.svg" alt="Version 0.2.2">
105
+ <img src="https://img.shields.io/badge/version-0.2.3-blue.svg" alt="Version 0.2.3">
106
106
  </p>
107
107
 
108
108
  `echr-py` is a research engineering toolkit for the case law of the European
@@ -112,10 +112,6 @@ Court of Human Rights. It connects public
112
112
  research artifacts through Python, a command-line interface, and the Model
113
113
  Context Protocol (MCP).
114
114
 
115
- The distribution and primary command are now named `echr-py`. The established
116
- `hudoc_py` Python import and the legacy `hudoc-py` command remain available for
117
- backward compatibility.
118
-
119
115
  It is designed for work where provenance matters. A result can retain the exact
120
116
  HUDOC document, language version, section, paragraph, individual opinion,
121
117
  footnote, citation span, cited procedural document, cited-paragraph pinpoint,
@@ -495,7 +491,7 @@ without cloning the repository:
495
491
 
496
492
  ```bash
497
493
  claude mcp add --scope user --transport stdio echr-py -- \
498
- "$(command -v uvx)" --from 'echr-py[mcp]==0.2.2' echr-py mcp
494
+ "$(command -v uvx)" --from 'echr-py[mcp]==0.2.3' echr-py mcp
499
495
  claude mcp get echr-py
500
496
  ```
501
497
 
@@ -635,6 +631,21 @@ cd echr-py
635
631
  python -m pip install -e ".[all,dev]"
636
632
  ```
637
633
 
634
+ ### Names
635
+
636
+ The distribution and primary command are `echr-py`. The Python import is
637
+ `hudoc_py`, and the earlier `hudoc-py` command still works; both remain
638
+ available so existing code and scripts keep running.
639
+
640
+ ```python
641
+ import hudoc_py # import name
642
+ ```
643
+
644
+ ```bash
645
+ echr-py --version # primary command
646
+ hudoc-py --version # legacy alias, same program
647
+ ```
648
+
638
649
  ## Documentation
639
650
 
640
651
  - [Worked examples](https://github.com/niccoloridi/echr-py/blob/main/docs/examples.md)
@@ -37,7 +37,7 @@ from ._sync import (
37
37
  from .main.dsl import Q
38
38
  from .models import Case, CaseCollection, Document, Sections
39
39
 
40
- __version__ = "0.2.2"
40
+ __version__ = "0.2.3"
41
41
 
42
42
  __all__ = [
43
43
  "__version__",
@@ -33,6 +33,7 @@ from .rescue import (
33
33
  find_french_sibling,
34
34
  rescue_candidates,
35
35
  rescue_french,
36
+ sibling_conflicts,
36
37
  )
37
38
 
38
39
 
@@ -70,6 +71,7 @@ __all__ = [
70
71
  "ReconcileStats",
71
72
  "ReconcileInvariantError",
72
73
  "rescue_french",
74
+ "sibling_conflicts",
73
75
  "rescue_candidates",
74
76
  "find_french_sibling",
75
77
  "apply_rescue_mapping",
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import asyncio
26
26
  import logging
27
- from datetime import UTC, datetime
27
+ from datetime import UTC, date, datetime
28
28
  from pathlib import Path
29
29
  from typing import Literal
30
30
 
@@ -86,6 +86,52 @@ def rescue_candidates(collection: list[Case]) -> list[Case]:
86
86
  return out
87
87
 
88
88
 
89
+ def _case_date(case: Case) -> date | None:
90
+ """The document's own decision date, whichever HUDOC field carries it."""
91
+ return case.kp_date or case.judgement_date or case.decision_date
92
+
93
+
94
+ def sibling_conflicts(case: Case, sibling: Case) -> list[str]:
95
+ """Reasons *sibling* cannot be the French counterpart of *case*.
96
+
97
+ An application number identifies an *application*, not a document. The same
98
+ number spans merits, just satisfaction, admissibility and revision
99
+ documents, so a sibling accepted on the number alone can silently pair an
100
+ English merits placeholder with a French admissibility decision.
101
+
102
+ Acceptance is therefore conjunctive and fail-closed: every identity the two
103
+ records both supply must agree. Evidence the sibling does not carry cannot
104
+ create a conflict, but it cannot excuse one either.
105
+ """
106
+ conflicts: list[str] = []
107
+
108
+ if case.doctype and sibling.doctype and case.doctype != sibling.doctype:
109
+ conflicts.append(f"doctype {case.doctype} != {sibling.doctype}")
110
+
111
+ if (
112
+ case.doctype_branch
113
+ and sibling.doctype_branch
114
+ and case.doctype_branch != sibling.doctype_branch
115
+ ):
116
+ conflicts.append(
117
+ f"doctype_branch {case.doctype_branch} != {sibling.doctype_branch}"
118
+ )
119
+
120
+ case_date, sibling_date = _case_date(case), _case_date(sibling)
121
+ if case_date and sibling_date and case_date != sibling_date:
122
+ conflicts.append(f"date {case_date.isoformat()} != {sibling_date.isoformat()}")
123
+
124
+ # A shared ECLI payload is the strongest available identity: the language
125
+ # segment differs between siblings, the rest must not.
126
+ if case.ecli and sibling.ecli and case.ecli != sibling.ecli:
127
+ conflicts.append(f"ecli {case.ecli} != {sibling.ecli}")
128
+
129
+ if case.appno and sibling.appno and not set(case.appno) & set(sibling.appno):
130
+ conflicts.append("no shared application number")
131
+
132
+ return conflicts
133
+
134
+
89
135
  async def find_french_sibling(
90
136
  client: AsyncHudocClient,
91
137
  case: Case,
@@ -98,7 +144,12 @@ async def find_french_sibling(
98
144
 
99
145
  Tries each application number in order; returns
100
146
  ``(french_itemid, appno_used, appnos_tried)`` for the first appno that
101
- yields a non-placeholder French document distinct from ``case.itemid``.
147
+ yields a non-placeholder French document distinct from ``case.itemid``
148
+ **and compatible with it** under :func:`sibling_conflicts`.
149
+
150
+ A candidate whose doctype, branch, date, ECLI or application set conflicts
151
+ is skipped rather than returned, so a run abstains instead of pairing an
152
+ English placeholder with the wrong procedural document.
102
153
  """
103
154
  tried: list[str] = []
104
155
  for appno in case.appno:
@@ -118,6 +169,15 @@ async def find_french_sibling(
118
169
  continue
119
170
  if sibling.is_placeholder is True:
120
171
  continue
172
+ conflicts = sibling_conflicts(case, sibling)
173
+ if conflicts:
174
+ logger.debug(
175
+ "rescue: rejected %s for %s (%s)",
176
+ sibling.itemid,
177
+ case.itemid,
178
+ "; ".join(conflicts),
179
+ )
180
+ continue
121
181
  return sibling.itemid, appno, tried
122
182
  return None, None, tried
123
183
 
@@ -66,6 +66,13 @@ _EN_CONTINUATION_RE = re.compile(
66
66
  re.VERBOSE,
67
67
  )
68
68
 
69
+ # Legacy conversions lowercase the names tail. Matched only through
70
+ # :func:`_continuation_names`, which requires roster anchoring for that form.
71
+ _EN_CONTINUATION_LOWER_RE = re.compile(
72
+ rf"^\s*(?:OF\s+)?(?:{_EN_TITLE})?(?P<names>\w.*?)\s*$",
73
+ re.VERBOSE | re.IGNORECASE,
74
+ )
75
+
69
76
  # --- French heading grammar --------------------------------------------------
70
77
 
71
78
  _FR_TYPE = r"""
@@ -238,8 +245,14 @@ def _classify_fr(type_full: str) -> OpinionType:
238
245
  def _looks_like_heading_tail(names: str) -> bool:
239
246
  """Accept a title-case heading but reject a sentence-shaped names tail."""
240
247
  tail = names.strip()
241
- if not tail or len(tail) >= 90 or tail.endswith((".", ";", ":")):
248
+ if not tail or tail.endswith((".", ";", ":")):
242
249
  return False
250
+ # The length ceiling separates a names tail from a wrapped prose sentence.
251
+ # A joint opinion of six or more judges legitimately exceeds it, so an
252
+ # over-long tail is not rejected outright: it must clear the roster anchor
253
+ # below, which prose cannot.
254
+ if len(tail) >= 90:
255
+ return _names_all_rostered(tail)
243
256
  # A proper name may be title case and contain connectors/particles, but a
244
257
  # prose continuation contains other wholly lowercase words.
245
258
  without_name_words = re.sub(
@@ -248,7 +261,31 @@ def _looks_like_heading_tail(names: str) -> bool:
248
261
  tail,
249
262
  flags=re.IGNORECASE,
250
263
  )
251
- return re.search(r"\b[a-zà-ÿ]{3,}\b", without_name_words) is None
264
+ if re.search(r"\b[a-zà-ÿ]{3,}\b", without_name_words) is None:
265
+ return True
266
+ # Legacy HUDOC conversions lowercase an otherwise exact names tail, which
267
+ # is indistinguishable from prose by casing alone. Accept that form only
268
+ # when every parsed name is independently anchored in the verified
269
+ # elected/ad-hoc roster, exactly as :func:`_heading_case_allowed` does for
270
+ # a lowercased single-line heading. Prose tails still fail closed.
271
+ return _names_all_rostered(tail)
272
+
273
+
274
+ def _names_all_rostered(tail: str) -> bool:
275
+ """True when every name in *tail* resolves in the canonical judge roster."""
276
+ from .judges import is_ad_hoc_judge, judge_country
277
+
278
+ stripped = re.sub(
279
+ r"^\s*(?:of|by|des|du|de)\s+(?:the\s+)?(?:judges?|juges?|mr|mrs|ms|sir)\b\.?\s*",
280
+ "",
281
+ tail,
282
+ flags=re.IGNORECASE,
283
+ )
284
+ authors, joined_by = _split_authors_joiners(stripped)
285
+ judges = [*authors, *joined_by]
286
+ return bool(judges) and all(
287
+ judge_country(judge) is not None or is_ad_hoc_judge(judge) for judge in judges
288
+ )
252
289
 
253
290
 
254
291
  def _heading_case_allowed(prefix: str, names: str) -> bool:
@@ -300,6 +337,24 @@ def _extend_names(lines: list[str], names: str, end: int, connectors: str) -> tu
300
337
  return names, end
301
338
 
302
339
 
340
+
341
+ def _continuation_names(strict: re.Pattern[str], lenient: re.Pattern[str], line: str) -> str | None:
342
+ """Names from a heading continuation line, or ``None``.
343
+
344
+ The strict pattern carries the ordinary uppercase contract. The lenient
345
+ pattern additionally accepts a lowercased tail, which legacy HUDOC
346
+ conversions produce, but only when every parsed name is anchored in the
347
+ verified elected/ad-hoc roster.
348
+ """
349
+ match = strict.match(line)
350
+ if match:
351
+ return match.group("names")
352
+ match = lenient.match(line)
353
+ if match and _names_all_rostered(match.group("names")):
354
+ return match.group("names")
355
+ return None
356
+
357
+
303
358
  def _match_header(lines: list[str], i: int) -> _Header | None:
304
359
  line = lines[i].strip()
305
360
  if not line or _TOC_LINE_RE.search(line):
@@ -344,11 +399,11 @@ def _match_header(lines: list[str], i: int) -> _Header | None:
344
399
  continuation = _next_continuation(lines, i) if match else None
345
400
  if match and continuation:
346
401
  j, continuation_line = continuation
347
- cont = _EN_CONTINUATION_RE.match(continuation_line)
348
- if cont and _heading_case_allowed(line, cont.group("names")):
349
- names, end = _extend_names(
350
- lines, cont.group("names"), j + 1, r"(?:,|\bAND)\s*$"
351
- )
402
+ cont_names = _continuation_names(
403
+ _EN_CONTINUATION_RE, _EN_CONTINUATION_LOWER_RE, continuation_line
404
+ )
405
+ if cont_names is not None and _heading_case_allowed(line, cont_names):
406
+ names, end = _extend_names(lines, cont_names, j + 1, r"(?:,|\bAND)\s*$")
352
407
  return _Header(
353
408
  i,
354
409
  end,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "echr-py"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Pythonic acquisition of HUDOC and HUDOC-EXEC metadata and source documents for ECtHR research."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes