pysec2pri 0.2.1__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 (51) hide show
  1. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/PKG-INFO +35 -13
  2. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/README.md +33 -11
  3. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/pyproject.toml +3 -3
  4. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/api.py +6 -14
  5. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/cli.py +19 -22
  6. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/ensembl.yaml +1 -9
  7. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/hgnc.yaml +1 -2
  8. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/ncbi.yaml +1 -10
  9. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/vgnc.yaml +0 -16
  10. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/consolidate.py +67 -68
  11. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/ensembl.py +12 -4
  12. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/chebi.py +3 -4
  13. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/hmdb.py +21 -36
  14. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/vgnc.py +2 -3
  15. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/version.py +1 -1
  16. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/LICENSE +0 -0
  17. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/__init__.py +0 -0
  18. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/__main__.py +0 -0
  19. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/__init__.py +0 -0
  20. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/chebi.yaml +0 -0
  21. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/hmdb_metabolites.yaml +0 -0
  22. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/hmdb_proteins.yaml +0 -0
  23. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/uniprot.yaml +0 -0
  24. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/config/wikidata.yaml +0 -0
  25. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/constants.py +0 -0
  26. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/download.py +0 -0
  27. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/__init__.py +0 -0
  28. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/chebi.py +0 -0
  29. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/hgnc.py +0 -0
  30. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/hmdb.py +0 -0
  31. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/ncbi.py +0 -0
  32. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/downloads/uniprot.py +0 -0
  33. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/exports.py +0 -0
  34. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/logging.py +0 -0
  35. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/__init__.py +0 -0
  36. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/base.py +0 -0
  37. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/ensembl.py +0 -0
  38. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/hgnc.py +0 -0
  39. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/ncbi.py +0 -0
  40. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/uniprot.py +0 -0
  41. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/parsers/wikidata.py +0 -0
  42. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/py.typed +0 -0
  43. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/__init__.py +0 -0
  44. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/chemical_redirects.rq +0 -0
  45. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/chemical_redirects_test.rq +0 -0
  46. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/gene_redirects.rq +0 -0
  47. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/gene_redirects_test.rq +0 -0
  48. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/protein_redirects.rq +0 -0
  49. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/queries/protein_redirects_test.rq +0 -0
  50. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/update_ids.py +0 -0
  51. {pysec2pri-0.2.1 → pysec2pri-0.2.3}/src/pysec2pri/xref.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysec2pri
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Secondary to primary identifier mapping
5
5
  Keywords: snekpack,cookiecutter
6
6
  Author: Javier Millán Acosta
@@ -40,7 +40,7 @@ Requires-Dist: numpy
40
40
  Requires-Dist: rdkit
41
41
  Requires-Dist: polars-runtime-32
42
42
  Requires-Dist: sssom-schema==1.1.0a5
43
- Requires-Dist: mapkgsutils>=0.0.5
43
+ Requires-Dist: mapkgsutils==0.1.1
44
44
  Maintainer: Javier Millán Acosta
45
45
  Maintainer-email: Javier Millán Acosta <javier.millan.acosta@gmail.com>
46
46
  Requires-Python: >=3.10
@@ -96,20 +96,33 @@ uv pip install git+https://github.com/jmillanacosta/pysec2pri.git
96
96
 
97
97
  ### Generating mapping sets
98
98
 
99
- To obtain the secondary to primary identifier SSSOM mapping set for ChEBI:
99
+ Mapping sets can be generated from bash:
100
100
 
101
101
  ```bash
102
- pysec2pri chebi ids
102
+ # ID mapping sets
103
+ pysec2pri chebi ids (--help)
104
+ pysec2pri ensembl ids (--help)
105
+ pysec2pri hgnc ids (--help)
106
+ pysec2pri vgnc ids (--help)
107
+ pysec2pri ncbi ids (--help)
108
+ pysec2pri hmdb-protein ids (--help)
109
+ pysec2pri hmdb-gene ids (--help)
110
+ pysec2pri uniprot ids (--help)
111
+ # Label mapping sets
112
+ pysec2pri chebi labels (--help)
113
+ pysec2pri hgnc labels (--help)
114
+ pysec2pri ensembl labels (--help)
115
+ pysec2pri vgnc labels (--help)
103
116
  ```
104
117
 
105
- This will automatically download the latest ChEBI release and generate an SSSOM
106
- mapping file in your current directory.
118
+ Or python:
107
119
 
108
- To process local files and specify the output:
109
-
110
- ```bash
111
- pysec2pri chebi ids ChEBI_complete_3star.sdf --output my_mappings.sssom.tsv
112
120
  ```
121
+ from pysec2pri import generate_ensembl_labels, generate_ensembl
122
+ ```
123
+
124
+ Replacing Ensembl by the supported database. These functions return either a
125
+ `IdMappingSet` or `LabelMappingSet`, SSSOM `MappingSet`s.
113
126
 
114
127
  For more options and help on any command:
115
128
 
@@ -122,14 +135,23 @@ The default output is in [SSSOM](https://mapping-commons.github.io/sssom/)
122
135
 
123
136
  ### Updating IDs and labels
124
137
 
125
- A generated mapping set can be used to update IDs and labels in Python:
138
+ A generated mapping set can be used to update IDs and labels in Python: ChEBI
139
+ synonyms:
126
140
 
127
141
  ```python
128
142
  from pysec2pri import generate_chebi_synonyms, resolve_labels
129
- cs = generate_chebi_synonyms()
143
+ chebi_ms = generate_chebi_synonyms()
130
144
  resolve_labels(["Glucose", "ATP", "Guanine"], cs)
131
145
  ```
132
146
 
147
+ Ensembl gene identifiers in a dataframe:
148
+
149
+ ```python
150
+ from pysec2pri import update_ids, generate_ensembl
151
+ ens_ms = generate_ensembl(version="115", species="9606")
152
+ df_with_new_column = update_ids(mapping_set=ens_ms, ids = df, at="Ensembl_id") # `at` is the name of the column
153
+ ```
154
+
133
155
  Or from the command line, given a TSV file `gene_ex.tsv`:
134
156
 
135
157
  ```
@@ -212,7 +234,7 @@ mappings.
212
234
  ### Aliases / synonyms
213
235
 
214
236
  Alias mappings use `oboInOwl:hasExactSynonym`. The alias is the `subject_label`
215
- and the authoritative name is the `object_label`/`object_id`.
237
+ and the primary name is the `object_label`/`object_id`.
216
238
 
217
239
  ```mermaid
218
240
  flowchart LR
@@ -43,20 +43,33 @@ uv pip install git+https://github.com/jmillanacosta/pysec2pri.git
43
43
 
44
44
  ### Generating mapping sets
45
45
 
46
- To obtain the secondary to primary identifier SSSOM mapping set for ChEBI:
46
+ Mapping sets can be generated from bash:
47
47
 
48
48
  ```bash
49
- pysec2pri chebi ids
49
+ # ID mapping sets
50
+ pysec2pri chebi ids (--help)
51
+ pysec2pri ensembl ids (--help)
52
+ pysec2pri hgnc ids (--help)
53
+ pysec2pri vgnc ids (--help)
54
+ pysec2pri ncbi ids (--help)
55
+ pysec2pri hmdb-protein ids (--help)
56
+ pysec2pri hmdb-gene ids (--help)
57
+ pysec2pri uniprot ids (--help)
58
+ # Label mapping sets
59
+ pysec2pri chebi labels (--help)
60
+ pysec2pri hgnc labels (--help)
61
+ pysec2pri ensembl labels (--help)
62
+ pysec2pri vgnc labels (--help)
50
63
  ```
51
64
 
52
- This will automatically download the latest ChEBI release and generate an SSSOM
53
- mapping file in your current directory.
65
+ Or python:
54
66
 
55
- To process local files and specify the output:
56
-
57
- ```bash
58
- pysec2pri chebi ids ChEBI_complete_3star.sdf --output my_mappings.sssom.tsv
59
67
  ```
68
+ from pysec2pri import generate_ensembl_labels, generate_ensembl
69
+ ```
70
+
71
+ Replacing Ensembl by the supported database. These functions return either a
72
+ `IdMappingSet` or `LabelMappingSet`, SSSOM `MappingSet`s.
60
73
 
61
74
  For more options and help on any command:
62
75
 
@@ -69,14 +82,23 @@ The default output is in [SSSOM](https://mapping-commons.github.io/sssom/)
69
82
 
70
83
  ### Updating IDs and labels
71
84
 
72
- A generated mapping set can be used to update IDs and labels in Python:
85
+ A generated mapping set can be used to update IDs and labels in Python: ChEBI
86
+ synonyms:
73
87
 
74
88
  ```python
75
89
  from pysec2pri import generate_chebi_synonyms, resolve_labels
76
- cs = generate_chebi_synonyms()
90
+ chebi_ms = generate_chebi_synonyms()
77
91
  resolve_labels(["Glucose", "ATP", "Guanine"], cs)
78
92
  ```
79
93
 
94
+ Ensembl gene identifiers in a dataframe:
95
+
96
+ ```python
97
+ from pysec2pri import update_ids, generate_ensembl
98
+ ens_ms = generate_ensembl(version="115", species="9606")
99
+ df_with_new_column = update_ids(mapping_set=ens_ms, ids = df, at="Ensembl_id") # `at` is the name of the column
100
+ ```
101
+
80
102
  Or from the command line, given a TSV file `gene_ex.tsv`:
81
103
 
82
104
  ```
@@ -159,7 +181,7 @@ mappings.
159
181
  ### Aliases / synonyms
160
182
 
161
183
  Alias mappings use `oboInOwl:hasExactSynonym`. The alias is the `subject_label`
162
- and the authoritative name is the `object_label`/`object_id`.
184
+ and the primary name is the `object_label`/`object_id`.
163
185
 
164
186
  ```mermaid
165
187
  flowchart LR
@@ -7,7 +7,7 @@ build-backend = "uv_build"
7
7
 
8
8
  [project]
9
9
  name = "pysec2pri"
10
- version = "0.2.1"
10
+ version = "0.2.3"
11
11
  description = "Secondary to primary identifier mapping"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -71,7 +71,7 @@ dependencies = [
71
71
  "rdkit", # Chemistry toolkit for SDF parsing
72
72
  "polars-runtime-32", # Polars runtime for 32-bit systems
73
73
  "sssom-schema==1.1.0a5", # SSSOM schema for validation
74
- "mapkgsutils>=0.0.5",
74
+ "mapkgsutils==0.1.1",
75
75
  ]
76
76
 
77
77
  # see https://peps.python.org/pep-0735/ and https://docs.astral.sh/uv/concepts/dependencies/#dependency-groups
@@ -255,7 +255,7 @@ known-first-party = [
255
255
  docstring-code-format = true
256
256
 
257
257
  [tool.bumpversion]
258
- current_version = "0.2.1"
258
+ current_version = "0.2.3"
259
259
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(?:-(?P<release>[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+(?P<build>[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"
260
260
  serialize = [
261
261
  "{major}.{minor}.{patch}-{release}+{build}",
@@ -244,8 +244,7 @@ def generate_hgnc_primary_ids(
244
244
  Only the HGNC complete set file is downloaded/read. The returned mapping
245
245
  set has an empty ``mappings`` list; its ``_primary_ids`` store is
246
246
  populated with every current HGNC ID so that ``to_pri_ids()`` produces
247
- the authoritative complete list, not just the subset of primaries that
248
- happen to have an associated secondary.
247
+ the complete list.
249
248
 
250
249
  Args:
251
250
  input_path: Local HGNC complete set TSV. Auto-downloaded if ``None``.
@@ -1001,7 +1000,7 @@ def generate_ensembl_labels(
1001
1000
  species: Canonical NCBI taxon ID, or ``"all"`` (default) to process
1002
1001
  every species Ensembl publishes and combine them into one
1003
1002
  mapping set -- see :func:`_generate_ensembl_all_species`. A
1004
- symbol shared by two species becomes genuinely ambiguous in
1003
+ symbol shared by two species becomes ambiguous in
1005
1004
  that combined output (there's no longer a per-species scope to
1006
1005
  disambiguate it). This is network-heavy (~276 species); pass
1007
1006
  an explicit taxon ID for a fast, single-species run.
@@ -1112,9 +1111,7 @@ def generate_ensembl_label_history(
1112
1111
  previous-symbol -> current-symbol transitions are derived by diffing
1113
1112
  each release's primary-label snapshot per stable ID. Delegates to
1114
1113
  :func:`pysec2pri.consolidate.build_label_history`, which walks every
1115
- historical release (or a bounded range) and resumes on re-run. This is
1116
- a network-heavy, run-on-demand operation, not part of normal
1117
- single-release generation.
1114
+ historical release (or a bounded range) and resumes on re-run.
1118
1115
 
1119
1116
  Args:
1120
1117
  species: Canonical NCBI taxon ID (default ``9606`` for human).
@@ -2015,15 +2012,10 @@ def crosswalk(
2015
2012
  ) -> dict[str, str] | pd.DataFrame:
2016
2013
  r"""Map a gene identifier from one vocabulary to another, via HGNC.
2017
2014
 
2018
- A thin convenience wrapper, not a separate resolver: ``frm="symbol"``
2019
- resolves through :func:`generate_hgnc_labels` +
2015
+ Wrapper: ``frm="symbol"`` resolves through :func:`generate_hgnc_labels` +
2020
2016
  :func:`~pysec2pri.update_ids.update_labels`, so a *previous* HGNC symbol
2021
- still resolves to its current identity (the temporal aspect) and a
2022
- genuinely ambiguous symbol is left blank and reported rather than
2023
- guessed. ``frm`` in ``"ensembl"``, ``"entrez"``, ``"refseq"``, or
2024
- ``"uniprot"`` resolves through HGNC's own cross-reference crosswalk
2025
- table, downloaded from the ``xref_sources`` declared in
2026
- ``config/hgnc.yaml`` unless *xref_mapping* is supplied explicitly.
2017
+ still resolves to its current identity (the temporal aspect) and an ambiguous
2018
+ label is left blank and reported rather than guessed.
2027
2019
 
2028
2020
  Args:
2029
2021
  input_data: A single identifier string, a list of identifier
@@ -464,7 +464,7 @@ def ensembl_label_history_cmd(
464
464
  Ensembl's core schema has no previous-gene-symbol table, so this walks
465
465
  every historical release (or a bounded --from-version/--to-version
466
466
  range) and diffs each release's primary-label snapshot to recover
467
- genuine previous -> current symbol transitions. Network-heavy and
467
+ true previous -> current symbol transitions. Network-heavy and
468
468
  resumable; run on demand, not part of normal mapping generation.
469
469
  """
470
470
  from pysec2pri.api import generate_ensembl_label_history
@@ -638,11 +638,11 @@ def _make_consolidate_cmd(cfg_id: str, extra_opts: list[Callable[..., Any]]) ->
638
638
  """Return a decorated (but not yet click.command-wrapped) ``consolidate`` callable."""
639
639
 
640
640
  def _cmd(
641
- mode: str,
642
641
  mapping_sets: str,
643
642
  cache_dir: Path | None,
644
643
  force: bool,
645
644
  no_progress: bool,
645
+ output: Path | None,
646
646
  **extra_kwargs: Any,
647
647
  ) -> None:
648
648
  from pysec2pri.consolidate import _sssom_output_path, consolidate_mapping_dates
@@ -650,34 +650,23 @@ def _make_consolidate_cmd(cfg_id: str, extra_opts: list[Callable[..., Any]]) ->
650
650
  extras = ", ".join(f"{k}={v}" for k, v in extra_kwargs.items() if v is not None)
651
651
  click.echo(
652
652
  f"Consolidating {cfg_id.upper()} mapping dates "
653
- f"(mode={mode}{', ' + extras if extras else ''}, {mapping_sets})..."
653
+ f"({extras + ', ' if extras else ''}{mapping_sets})..."
654
654
  )
655
655
  try:
656
656
  path, _ = consolidate_mapping_dates(
657
657
  cfg_id,
658
- mode=mode,
659
658
  cache_dir=cache_dir,
660
659
  mapping_sets=mapping_sets,
661
660
  show_progress=not no_progress,
662
661
  force=force,
662
+ output=output,
663
663
  **extra_kwargs,
664
664
  )
665
665
  except ValueError as exc:
666
666
  raise click.ClickException(str(exc)) from exc
667
- click.echo(f"Wrote consolidated mapping set -> {_sssom_output_path(path)}")
667
+ click.echo(f"Wrote consolidated mapping set -> {output or _sssom_output_path(path)}")
668
668
 
669
669
  decorators: list[Callable[..., Any]] = [
670
- click.option(
671
- "--mode",
672
- default="release",
673
- show_default=True,
674
- type=click.Choice(["release", "date"]),
675
- help=(
676
- "'release': walk every historical release for the first-seen version/date. "
677
- "'date': single pass using the source's own per-row date (falls back to "
678
- "'release' with a warning if the source has none)."
679
- ),
680
- ),
681
670
  *extra_opts,
682
671
  click.option(
683
672
  "--mapping-sets",
@@ -693,6 +682,13 @@ def _make_consolidate_cmd(cfg_id: str, extra_opts: list[Callable[..., Any]]) ->
693
682
  default=False,
694
683
  help="Re-scan every release, ignoring resume state.",
695
684
  ),
685
+ click.option(
686
+ "-o",
687
+ "--output",
688
+ type=PathType,
689
+ default=None,
690
+ help="Also write the consolidated SSSOM mapping set to this path.",
691
+ ),
696
692
  _opt_no_progress,
697
693
  ]
698
694
  for dec in reversed(decorators):
@@ -718,12 +714,13 @@ def _register_consolidate_commands(parent: click.Group) -> None:
718
714
  "Build/update the per-mapping first-seen-date index, written as a real "
719
715
  "SSSOM mapping set whose mapping_date is each mapping's date of first "
720
716
  "appearance.\n\n"
721
- "In 'release' mode (default), walks every historical release once to "
722
- "discover the earliest release each mapping appeared in. This is slow "
723
- "and network-heavy (~250 releases for ChEBI); meant to be run as a "
724
- "manual/one-off operation, not as part of normal mapping generation. "
725
- "In 'date' mode, a single current-release pass captures the source's "
726
- "own per-row date directly (fast)."
717
+ "For datasources with a versioned archive (ChEBI, Ensembl, HGNC, "
718
+ "UniProt), walks every historical release once to discover the earliest "
719
+ "release each mapping appeared in. This is slow and network-heavy (~250 "
720
+ "releases for ChEBI); meant to be run as a manual/one-off operation, not "
721
+ "as part of normal mapping generation. For datasources without one "
722
+ "(NCBI, VGNC), a single current-release pass captures the source's own "
723
+ "per-row date directly (fast)."
727
724
  ),
728
725
  )(raw_cmd)
729
726
  )
@@ -7,13 +7,6 @@ prefix: "ENSEMBL"
7
7
  curie_base_url: "http://identifiers.org/ensembl/"
8
8
  entity_types: ["genes"]
9
9
 
10
- # Canonical key = NCBI taxon ID. `token` is Ensembl's own species directory
11
- # component (e.g. https://ftp.ensembl.org/pub/release-115/mysql/homo_sapiens_core_115_38/).
12
- # `default: "all"` processes every species Ensembl publishes for the
13
- # release and combines them into one mapping set (see
14
- # pysec2pri.api._generate_ensembl_all_species) -- network-heavy (~276
15
- # species, one download each); pass an explicit taxon ID for a fast,
16
- # single-species run.
17
10
  species:
18
11
  default: "all"
19
12
  available:
@@ -511,5 +504,4 @@ xref_sources:
511
504
  refseq: "RefSeq IDs"
512
505
  uniprot: "UniProt ID(supplied by UniProt)"
513
506
  note:
514
- "Source of truth for cross-references varies; this is a suggested default,
515
- not authoritative."
507
+ "Source of truth for cross-references varies; this is a suggested default."
@@ -142,8 +142,7 @@ xref_sources:
142
142
  refseq: "RefSeq IDs"
143
143
  uniprot: "UniProt ID(supplied by UniProt)"
144
144
  note:
145
- "Source of truth for cross-references varies; this is a suggested default,
146
- not authoritative."
145
+ "Source of truth for cross-references varies; this is a suggested default"
147
146
 
148
147
  mapping_sets:
149
148
  ids:
@@ -9,14 +9,6 @@ prefix: "NCBIGene"
9
9
  curie_base_url: "http://identifiers.org/ncbigene/"
10
10
  entity_types: ["genes"]
11
11
 
12
- # NCBI's own token IS the canonical taxon ID (no translation needed, unlike
13
- # Ensembl) -- gene_history/gene_info are filtered directly on #tax_id, which
14
- # accepts ANY taxon ID, not just the ones listed here. `available` exists
15
- # only to drive CLI help/choices for common species; NCBIParser never calls
16
- # species_token(), so it never restricts which taxon IDs are accepted.
17
- # `default: "all"` processes every organism in the file instead of
18
- # filtering to one (see NCBIParser.ALL_SPECIES) -- cheap here since
19
- # gene_info/gene_history are single global files already being downloaded.
20
12
  species:
21
13
  default: "all"
22
14
  available:
@@ -173,5 +165,4 @@ xref_sources:
173
165
  refseq: "RefSeq IDs"
174
166
  uniprot: "UniProt ID(supplied by UniProt)"
175
167
  note:
176
- "Source of truth for cross-references varies; this is a suggested default,
177
- not authoritative."
168
+ "Source of truth for cross-references varies; this is a suggested default"
@@ -7,22 +7,6 @@ prefix: "VGNC"
7
7
  curie_base_url: "http://identifiers.org/vgnc/"
8
8
  entity_types: ["genes"]
9
9
 
10
- # VGNC (Vertebrate Gene Nomenclature Committee) names genes for vertebrate
11
- # species that lack their own nomenclature committee (human/mouse/rat are
12
- # deliberately out of scope -- HGNC/MGI/RGD cover those). Unlike Ensembl
13
- # (one download URL per species), VGNC publishes a single global TSV per
14
- # file with a `taxon_id` column, filtered the same way NCBI's
15
- # gene_info/gene_history are filtered by `#tax_id` -- see VGNCParser.
16
- # `default: "all"` processes every species together (see
17
- # VGNCParser.ALL_SPECIES) -- cheap for ids (the global withdrawn set is
18
- # always parsed in full regardless), heavier for labels (a symbol shared
19
- # by two species becomes genuinely ambiguous with no per-species scope to
20
- # disambiguate it). VGNCParser never calls species_token(), so `available`
21
- # exists only to drive CLI help/choices and never restricts which taxon IDs
22
- # are accepted. Taxon IDs below were verified against the live data (each
23
- # species' own vgnc/tsv/<species>/*_vgnc_gene_set_All.tsv, cross-referenced
24
- # by VGNC ID against vgnc/tsv/all/all_vgnc_gene_set_All.tsv's taxon_id
25
- # column).
26
10
  species:
27
11
  default: "all"
28
12
  available:
@@ -40,7 +40,6 @@ import json
40
40
  import os
41
41
  import shutil
42
42
  import tempfile
43
- import warnings
44
43
  from collections.abc import Callable, Iterable
45
44
  from pathlib import Path
46
45
  from typing import Any
@@ -60,7 +59,7 @@ __all__ = [
60
59
  ]
61
60
 
62
61
  # Datasources this module knows how to download+parse per release.
63
- # NCBI and VGNC have no versioned archive, so only mode="date" applies to them.
62
+ # NCBI and VGNC have no versioned archive, so they take the single-parse path.
64
63
  SUPPORTED_DATASOURCES = ("chebi", "ensembl", "hgnc", "ncbi", "uniprot", "vgnc")
65
64
 
66
65
  # mapping_sets kinds each datasource's parser actually supports.
@@ -100,7 +99,8 @@ def _uniprot_versions(**kwargs: Any) -> list[str]:
100
99
  return UniProtDownloader().list_versions()
101
100
 
102
101
 
103
- # Datasources with a versioned archive that can be walked for mode="release".
102
+ # Datasources with a versioned archive that can be walked release by release.
103
+ # Datasources absent here (NCBI, VGNC) take the single-parse path instead.
104
104
  _LIST_VERSIONS_FNS: dict[str, Callable[..., list[str]]] = {
105
105
  "chebi": _chebi_versions,
106
106
  "ensembl": _ensembl_versions,
@@ -161,6 +161,12 @@ _read_meta = _consolidate.read_meta
161
161
  _write_meta = _consolidate.write_meta
162
162
 
163
163
 
164
+ def _save_optional_output(mapping_set: BaseMappingSet, output: Path | None) -> None:
165
+ """Write *mapping_set* to *output* as SSSOM when an output path is given."""
166
+ if output is not None:
167
+ mapping_set.save("sssom", output)
168
+
169
+
164
170
  def load_mapping_dates(
165
171
  datasource: str,
166
172
  cache_dir: Path | None = None,
@@ -382,48 +388,48 @@ def _write_consolidated_sssom(
382
388
  def consolidate_mapping_dates(
383
389
  datasource: str,
384
390
  *,
385
- mode: str = "release",
386
391
  cache_dir: Path | None = None,
387
392
  mapping_sets: str = "ids",
388
393
  show_progress: bool = True,
389
394
  force: bool = False,
395
+ output: Path | None = None,
390
396
  **kwargs: Any,
391
397
  ) -> tuple[Path, BaseMappingSet]:
392
398
  """Build/update the first-seen-date index for *datasource*.
393
399
 
394
- Two strategies, selected via *mode*:
395
-
396
- - ``"release"``: walk every historical release of *datasource* once
397
- (oldest first, resuming from the last completed version unless
398
- *force* is set). For every mapping seen, records the version/date it
399
- first appeared and keeps bumping the version/date it was last seen.
400
- This is a slow, network-heavy operation (~250 releases for ChEBI) and
401
- is meant to be run manually/as a one-off, not as part of normal
402
- mapping generation. Requires a versioned archive (ChEBI, HGNC,
403
- UniProt); raises :class:`ValueError` for datasources without one
404
- (e.g. NCBI).
405
- - ``"date"``: a single (current) parse, capturing whatever real
406
- per-row ``mapping_date`` the datasource's parser already produces
407
- (e.g. HGNC's ``date_symbol_changed``, NCBI's ``Discontinue_Date``).
408
- Fast -- no historical walk. If *datasource* never produces a real
409
- per-row date (ChEBI, UniProt), falls back to ``"release"`` mode and
410
- warns the user.
411
-
412
- Either way, alongside the internal cache file this also writes a
413
- companion real SSSOM mapping set (see :func:`_sssom_output_path`) where
414
- every row's ``mapping_date`` is its true first-seen date, rather than
415
- whichever release happened to be parsed last.
400
+ Collects every mapping using all available provenance. The walk shape is
401
+ chosen automatically by data availability, not by a requested mode:
402
+
403
+ - Datasources **with** a versioned archive (ChEBI, Ensembl, HGNC,
404
+ UniProt): walk every historical release once (oldest first, resuming
405
+ from the last completed version unless *force*). For every mapping
406
+ seen, records the version/date it first appeared and keeps bumping the
407
+ version/date it was last seen. Slow and network-heavy (~250 releases
408
+ for ChEBI); meant to be run manually/as a one-off.
409
+ - Datasources **without** one (NCBI, VGNC): a single current parse
410
+ caching the *full* mapping set — every mapping kept, stamped with its
411
+ own per-row ``mapping_date`` when the parser provides one and left
412
+ undated otherwise. Fast, no historical walk.
413
+
414
+ Alongside the internal cache file this also writes a companion real SSSOM
415
+ mapping set (see :func:`_sssom_output_path`) where each row's
416
+ ``mapping_date`` is its per-row date when present, else its first-seen
417
+ release date — distinct from the first-seen release version carried in
418
+ ``subject_source_version``/``object_source_version``.
416
419
 
417
420
  Args:
418
421
  datasource: One of :data:`SUPPORTED_DATASOURCES`.
419
- mode: ``"release"`` or ``"date"``.
420
422
  cache_dir: Directory to write the cache file. Defaults to
421
423
  :func:`default_cache_dir`.
422
424
  mapping_sets: ``"ids"`` or ``"labels"``.
423
425
  show_progress: Whether to show a progress bar over releases
424
- (``"release"`` mode only).
426
+ (versioned-archive datasources only).
425
427
  force: Re-scan every release from scratch, ignoring any existing
426
- cache/resume state (``"release"`` mode only).
428
+ cache/resume state (versioned-archive datasources only).
429
+ output: Optional path to also write the consolidated SSSOM mapping
430
+ set to. The internal cache-adjacent copy (see
431
+ :func:`_sssom_output_path`) is always written; when *output* is
432
+ given, the same full mapping set is additionally saved there.
427
433
  **kwargs: Datasource-specific knobs (``subset`` for ChEBI, ``species``
428
434
  for NCBI/Ensembl); ignored for datasources with no such config
429
435
  block.
@@ -434,13 +440,9 @@ def consolidate_mapping_dates(
434
440
  written alongside it) and the in-memory consolidated mapping set.
435
441
 
436
442
  Raises:
437
- ValueError: For an unknown *mode*, an unsupported *datasource*, an
438
- unsupported *mapping_sets* for *datasource*, or
439
- ``mode="release"`` against a datasource with no versioned
440
- archive.
443
+ ValueError: For an unsupported *datasource* or an unsupported
444
+ *mapping_sets* for *datasource*.
441
445
  """
442
- if mode not in ("release", "date"):
443
- raise ValueError(f"mode must be 'release' or 'date', got {mode!r}")
444
446
  if datasource not in SUPPORTED_DATASOURCES:
445
447
  raise ValueError(
446
448
  f"Unsupported datasource: {datasource!r}. Supported: {SUPPORTED_DATASOURCES}"
@@ -465,46 +467,43 @@ def consolidate_mapping_dates(
465
467
  cache_path = _cache_path(cache_dir, datasource, mapping_sets, **kwargs)
466
468
  meta_path = _meta_path(cache_dir, datasource, mapping_sets, **kwargs)
467
469
 
468
- if mode == "date":
469
- got_dates = _consolidate.consolidate_by_date(
470
- cache_path,
471
- meta_path,
472
- run_one_version=lambda: _run_one_version(datasource, None, mapping_sets, **kwargs),
473
- )
474
- if got_dates:
475
- _, mapping_set = _write_consolidated_sssom(
476
- datasource, mapping_sets, cache_path, meta_path
477
- )
478
- return cache_path, mapping_set
479
- msg = (
480
- f"{datasource!r} has no parseable per-row mapping dates; "
481
- "falling back to mode='release'."
482
- )
483
- logger.warning(msg)
484
- warnings.warn(msg, UserWarning, stacklevel=2)
470
+ def _run(version: str | None) -> Any:
471
+ return _run_one_version(datasource, version, mapping_sets, **kwargs)
485
472
 
486
473
  list_versions_fn = _LIST_VERSIONS_FNS.get(datasource)
487
474
  if list_versions_fn is None:
488
- raise ValueError(f"{datasource!r} has no versioned archive; only mode='date' is supported.")
475
+ # No versioned archive (NCBI, VGNC): single current parse, full set.
476
+ _consolidate.consolidate(
477
+ cache_path,
478
+ meta_path,
479
+ label=datasource,
480
+ run_one_version=_run,
481
+ show_progress=show_progress,
482
+ force=force,
483
+ )
484
+ else:
485
+ # Versioned archive: walk every historical release for first-seen dates.
486
+ from pysec2pri.download import resolve_release_date
489
487
 
490
- from pysec2pri.download import resolve_release_date
488
+ fn = list_versions_fn
489
+ _consolidate.consolidate(
490
+ cache_path,
491
+ meta_path,
492
+ label=datasource,
493
+ run_one_version=_run,
494
+ list_versions=lambda: fn(**kwargs),
495
+ resolve_release_date=lambda v: resolve_release_date(datasource, v, **kwargs),
496
+ show_progress=show_progress,
497
+ force=force,
498
+ )
491
499
 
492
- _consolidate.consolidate_by_release(
493
- cache_path,
494
- meta_path,
495
- label=datasource,
496
- list_versions=lambda: list_versions_fn(**kwargs),
497
- run_one_version=lambda v: _run_one_version(datasource, v, mapping_sets, **kwargs),
498
- resolve_release_date=lambda v: resolve_release_date(datasource, v, **kwargs),
499
- show_progress=show_progress,
500
- force=force,
501
- )
502
500
  _, mapping_set = _write_consolidated_sssom(datasource, mapping_sets, cache_path, meta_path)
501
+ _save_optional_output(mapping_set, output)
503
502
  return cache_path, mapping_set
504
503
 
505
504
 
506
505
  # Cross-release label history (e.g. Ensembl, whose core schema has no
507
- # previous-gene-symbol table -- genuine previous->current symbol transitions
506
+ # previous-gene-symbol table: previous->current symbol transitions
508
507
  # are recovered by diffing each release's current-label snapshot).
509
508
 
510
509
 
@@ -571,8 +570,8 @@ def build_label_history(
571
570
  previous release's, oldest to newest. Network-heavy and resumable --
572
571
  meant to be run on demand, not as part of normal mapping generation
573
572
  (mirrors the release-walk pattern in
574
- :func:`mapkgsutils.consolidate.consolidate_by_release`, but tracks a
575
- carried label map instead of first/last-seen dates).
573
+ :func:`mapkgsutils.consolidate.consolidate`, but tracks a carried label
574
+ map instead of first/last-seen dates).
576
575
 
577
576
  Args:
578
577
  datasource: Currently only ``"ensembl"`` is supported.
@@ -648,7 +647,7 @@ def build_label_history(
648
647
  shutil.rmtree(tmpdir, ignore_errors=True)
649
648
 
650
649
  release_date = resolve_release_date(datasource, v, species=species)
651
- # Unlike consolidate_by_release's plain-dict cache, these rows become
650
+ # Unlike the consolidate walk's plain-dict cache, these rows become
652
651
  # real Mapping objects -- mapping_date must be a valid date or None,
653
652
  # never the raw (non-date-shaped) version string.
654
653
  date_str = release_date.date().isoformat() if release_date else None
@@ -375,8 +375,16 @@ def urls_and_date(
375
375
  urls = _get_ensembl_urls_for_version(version, species=species)
376
376
  logger.info("Ensembl version %s (species %s): %s", version, species, urls)
377
377
  else:
378
- release_info = check_ensembl_release()
379
- urls = release_info.files
380
- release_date = release_info.release_date
381
- logger.info("Ensembl release %s: %s", release_info.version, urls)
378
+ # The release number is global across species, but the URLs (and the
379
+ # resolved release date) are species-specific, so resolve the latest
380
+ # version and build URLs for the requested species rather than reusing
381
+ # check_ensembl_release()'s human-only URLs.
382
+ versions = EnsemblDownloader(show_progress=False).list_versions()
383
+ if not versions:
384
+ raise ValueError("Could not find Ensembl releases on the FTP server")
385
+ version = versions[-1]
386
+ urls = _get_ensembl_urls_for_version(version, species=species)
387
+ check_url = urls.get("stable_id_event")
388
+ release_date = get_file_last_modified(check_url) if check_url else None
389
+ logger.info("Ensembl release %s (species %s): %s", version, species, urls)
382
390
  return urls, release_date
@@ -190,8 +190,7 @@ class ChEBIParser(BaseParser):
190
190
  Reads ``compounds.tsv`` (TSV releases >= 245) to extract every current
191
191
  ChEBI compound ID. The returned mapping set has an empty ``mappings``
192
192
  list; its ``_primary_ids`` store is populated with every current ChEBI
193
- ID (CHEBI: prefixed) so that ``to_pri_ids()`` produces the authoritative
194
- complete list.
193
+ ID (CHEBI: prefixed) so that ``to_pri_ids()`` produces the complete list.
195
194
 
196
195
  Args:
197
196
  input_path: Path to a directory containing ``compounds.tsv``, or
@@ -496,7 +495,7 @@ def _parse_names_tsv(
496
495
 
497
496
  Format: id|compound_id|name|type|status_id|adapted|language_code|ascii_name
498
497
 
499
- The authoritative primary (canonical) name for each compound is taken from
498
+ The true primary (canonical) name for each compound is taken from
500
499
  the ``name`` column of ``compounds.tsv``: the same value displayed on the
501
500
  ChEBI website and stored as the top-level label in the ChEBI ontology.
502
501
  Every entry in ``names.tsv`` that differs from that canonical name is
@@ -526,7 +525,7 @@ def _parse_names_tsv(
526
525
  "Pass the path to compounds.tsv."
527
526
  )
528
527
 
529
- # Load canonical names from compounds.tsv (ChEBI-authoritative primary label)
528
+ # Load canonical names from compounds.tsv (ChEBI primary label)
530
529
  cpd_cols = ["id", "name", "stars"] if subset == "3star" else ["id", "name"]
531
530
  cpd_df = pl.read_csv(
532
531
  compounds_path,
@@ -17,7 +17,6 @@ import gzip
17
17
  import io
18
18
  import re
19
19
  import zipfile
20
- from collections.abc import Callable
21
20
  from pathlib import Path
22
21
  from typing import IO, TYPE_CHECKING
23
22
 
@@ -85,7 +84,6 @@ class HMDBParser(BaseParser):
85
84
  element_tag: str,
86
85
  prefix: str,
87
86
  desc: str,
88
- secondary_normaliser: Callable[[str], str] | None = None,
89
87
  ) -> BaseMappingSet:
90
88
  """Parse an HMDB XML file and return a mapping set.
91
89
 
@@ -95,8 +93,6 @@ class HMDBParser(BaseParser):
95
93
  ``"protein"``).
96
94
  prefix: CURIE prefix (``"HMDB"`` for both record types).
97
95
  desc: Progress-bar description string.
98
- secondary_normaliser: Optional callable ``(raw_str) -> str``
99
- applied to each raw secondary accession before prefixing.
100
96
 
101
97
  Returns:
102
98
  :class:`BaseMappingSet` with computed cardinalities and
@@ -130,10 +126,10 @@ class HMDBParser(BaseParser):
130
126
  accession_elem = elem.find("accession")
131
127
  if accession_elem is not None and accession_elem.text:
132
128
  primary_ids_found.add(f"{prefix}:{accession_elem.text.strip()}")
133
- rows_data.extend(self._process_record(elem, prefix, secondary_normaliser))
129
+ rows_data.extend(self._process_record(elem, prefix))
134
130
  elem.clear()
135
131
  except DefusedET.ParseError:
136
- rows_data = self._parse_simple_xml(file_path, element_tag, prefix, secondary_normaliser)
132
+ rows_data = self._parse_simple_xml(file_path, element_tag, prefix)
137
133
 
138
134
  mappings = self._build_mappings(
139
135
  rows_data, fixed, desc="Building HMDB mappings", total=len(rows_data)
@@ -147,9 +143,14 @@ class HMDBParser(BaseParser):
147
143
  self,
148
144
  elem: Element,
149
145
  prefix: str,
150
- secondary_normaliser: Callable[[str], str] | None,
151
146
  ) -> list[dict[str, str]]:
152
- """Extract row dicts from a single record element."""
147
+ """Extract row dicts from a single record element.
148
+
149
+ Bare-numeric ``secondary_accessions`` entries are skipped: in the
150
+ proteins file these are HMDB's internal database row ids (not legacy
151
+ accessions), and a bare integer is never a valid HMDB accession for
152
+ either record type. See issue #44.
153
+ """
153
154
  # Primary accession
154
155
  accession_elem = elem.find("hmdb:accession", HMDB_NS)
155
156
  if accession_elem is None:
@@ -177,8 +178,8 @@ class HMDBParser(BaseParser):
177
178
  if not sec_elem.text:
178
179
  continue
179
180
  raw_sec = sec_elem.text.strip()
180
- if secondary_normaliser is not None:
181
- raw_sec = secondary_normaliser(raw_sec)
181
+ if _BARE_NUM_RE.match(raw_sec):
182
+ continue
182
183
  secondary_id = f"{prefix}:{raw_sec}"
183
184
  rows.append(
184
185
  {
@@ -200,7 +201,6 @@ class HMDBParser(BaseParser):
200
201
  file_path: Path,
201
202
  element_tag: str,
202
203
  prefix: str,
203
- secondary_normaliser: Callable[[str], str] | None,
204
204
  ) -> list[dict[str, str]]:
205
205
  """Fallback parser for test files without a namespace declaration."""
206
206
  rows_data: list[dict[str, str]] = []
@@ -208,7 +208,7 @@ class HMDBParser(BaseParser):
208
208
  tree = DefusedET.parse(file_path)
209
209
  root = tree.getroot()
210
210
  for record in root.findall(f".//{element_tag}"):
211
- rows_data.extend(self._process_record(record, prefix, secondary_normaliser))
211
+ rows_data.extend(self._process_record(record, prefix))
212
212
  except Exception as e:
213
213
  logger.warning("Failed to parse HMDB XML: %s", e)
214
214
  return rows_data
@@ -329,9 +329,15 @@ class HMDBProteinParser(HMDBParser):
329
329
 
330
330
  Reads configuration from ``hmdb_proteins.yaml``.
331
331
  Primary accessions have the form ``HMDBP…`` (e.g. ``HMDBP00001``).
332
- Secondary accessions may be bare numbers (legacy format, e.g. ``5229``)
333
- or full ``HMDBP`` accessions; both are normalised by
334
- :meth:`_normalise_protein_secondary`.
332
+
333
+ A record's ``<secondary_accessions>`` block holds two distinct kinds of
334
+ value. Only the first is a real identifier:
335
+
336
+ * **``HMDBP``-prefixed accessions** (e.g. ``HMDBP05261``): genuine retired
337
+ accessions that map to the record's primary accession.
338
+ * **Bare numeric values** (e.g. ``5229``): HMDB's internal database row id,
339
+ *not* a legacy accession. These are dropped by the shared
340
+ :meth:`~HMDBParser._process_record` (see issue #44).
335
341
  """
336
342
 
337
343
  datasource_name = "hmdb_proteins"
@@ -341,26 +347,6 @@ class HMDBProteinParser(HMDBParser):
341
347
  """Proteins download URL from ``hmdb_proteins.yaml``."""
342
348
  return self.get_download_url("proteins") or ""
343
349
 
344
- @staticmethod
345
- def _normalise_protein_secondary(raw: str) -> str:
346
- """Normalise a raw secondary protein accession to a ``HMDBP``-prefixed form.
347
-
348
- Bare numeric values (legacy IDs) are zero-padded to five digits and
349
- prefixed with ``HMDBP`` to match the primary accession pattern.
350
- Already-prefixed values are returned unchanged.
351
-
352
- Args:
353
- raw: Raw secondary accession string (e.g. ``"5229"`` or
354
- ``"HMDBP05229"``).
355
-
356
- Returns:
357
- Normalised accession string (e.g. ``"HMDBP05229"``).
358
- """
359
- raw = raw.strip()
360
- if _BARE_NUM_RE.match(raw):
361
- return f"HMDBP{int(raw):05d}"
362
- return raw
363
-
364
350
  def parse(self, input_path: Path | str | None) -> BaseMappingSet:
365
351
  """Parse ``hmdb_proteins.xml`` (or ``.zip`` / ``.gz``).
366
352
 
@@ -377,7 +363,6 @@ class HMDBProteinParser(HMDBParser):
377
363
  element_tag="protein",
378
364
  prefix="HMDBP",
379
365
  desc="Parsing HMDB proteins XML",
380
- secondary_normaliser=self._normalise_protein_secondary,
381
366
  )
382
367
 
383
368
 
@@ -25,9 +25,8 @@ This parser extracts:
25
25
  detection and ``to_pri_labels()`` stay scoped to one species' namespace
26
26
  instead of flagging cross-species homonyms as ambiguous. Passing
27
27
  :data:`ALL_SPECIES` processes every species together instead, in which
28
- case a shared symbol *is* genuinely ambiguous (there's no other context
29
- to tell the species apart) and gets flagged accordingly -- that's
30
- ``_annotate_ambiguous_mappings`` doing its job, not a bug.
28
+ case a shared symbol is really ambiguous (there's no other context
29
+ to tell the species apart) and gets flagged accordingly.
31
30
 
32
31
  Uses SSSOM-compliant MappingSet classes with cardinality computation.
33
32
  """
@@ -12,7 +12,7 @@ __all__ = [
12
12
  "get_version",
13
13
  ]
14
14
 
15
- VERSION = "0.2.1"
15
+ VERSION = "0.2.3"
16
16
 
17
17
 
18
18
  def get_git_hash() -> str:
File without changes