cvgruplac-json 0.1.0__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.
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ # Add here any generated outputs or data files that hold personal data (e.g. downloaded pages)
14
+ # Never commit credentials
15
+ .env
16
+ .pypirc
17
+ *apikey*
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-15
8
+
9
+ ### Added
10
+ - First version: `cvgruplac-json --cvlac … --gruplac … --out …` and `link`, `link_gruplac`, `link_cvlac`, `id_hash`.
11
+ Every product of a GrupLAC (gruplac-json) and of a CvLAC (cvlac-json) gets `id` and `scrapping_ids`
12
+ (`{id, source}`; `verified: false` on cvlac ids that could not be checked; `group` on gruplac ids in a CvLAC).
13
+ GrupLAC products copy the first verified member CvLAC id; the others, and CvLAC events, get `id_hash`, the
14
+ product_hash recipe with the group code or the COD_RH and the extra kind `event`. Section pairing of CvLAC and
15
+ GrupLAC in `data/sections.json`. Gzipped outputs are written at level 6.
16
+
17
+ [Unreleased]: https://github.com/restrepo/cvgruplac-json/compare/v0.1.0...HEAD
18
+ [0.1.0]: https://github.com/restrepo/cvgruplac-json/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Diego Restrepo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.5
2
+ Name: cvgruplac-json
3
+ Version: 0.1.0
4
+ Summary: Link the JSONs of cvlac-json and gruplac-json: one id per product, in both directions.
5
+ Project-URL: Homepage, https://github.com/restrepo/cvgruplac-json
6
+ Project-URL: Repository, https://github.com/restrepo/cvgruplac-json
7
+ Project-URL: Issues, https://github.com/restrepo/cvgruplac-json/issues
8
+ Project-URL: Changelog, https://github.com/restrepo/cvgruplac-json/blob/main/CHANGELOG.md
9
+ Author-email: Diego Restrepo <restrepo@udea.edu.co>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: build>=1.2; extra == 'dev'
21
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
22
+ Requires-Dist: pytest>=8; extra == 'dev'
23
+ Requires-Dist: ruff==0.17.3; extra == 'dev'
24
+ Requires-Dist: twine>=5; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # cvgruplac-json
28
+
29
+ [![CI](https://github.com/restrepo/cvgruplac-json/actions/workflows/ci.yml/badge.svg)](https://github.com/restrepo/cvgruplac-json/actions/workflows/ci.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/cvgruplac-json.svg)](https://pypi.org/project/cvgruplac-json/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/cvgruplac-json.svg)](https://pypi.org/project/cvgruplac-json/)
32
+
33
+ Une los JSON de [cvlac-json](https://github.com/restrepo/cvlac-json) (el CvLAC de una persona) y de
34
+ [gruplac-json](https://github.com/restrepo/gruplac-json) (el GrupLAC de un grupo) de Minciencias: cada producto de
35
+ los dos recibe un `id` y una lista `scrapping_ids` con sus ids en una y otra fuente, así que un producto del grupo
36
+ apunta al mismo producto en el CV de cada integrante que lo firmó, y al revés.
37
+
38
+ No descarga nada ni llama a ninguna API: lee los JSON que ya escribieron los dos paquetes y escribe copias unidas,
39
+ sin tocar los originales. Solo usa la biblioteca estándar.
40
+
41
+ ## Instalación
42
+
43
+ ```bash
44
+ pip install cvgruplac-json
45
+ ```
46
+
47
+ ## Uso
48
+
49
+ ```bash
50
+ cvgruplac-json --cvlac cvlac/ --gruplac gruplac/ --out unidos/
51
+ cvgruplac-json --cvlac cvlac_udea/ cvlac_colombia/json/ --gruplac gruplac/json/ --out unidos/ --workers 4
52
+ cvgruplac-json --gruplac gruplac_COL0008423.json --cvlac cvlac_0000707619.json --out unidos/
53
+ cvgruplac-json --gruplac gruplac/ --out unidos/ # sin CvLAC: ids de CvLAC sin verificar
54
+ ```
55
+
56
+ - `--cvlac` y `--gruplac` reciben archivos o carpetas (`cvlac_*.json[.gz]`, `gruplac_*.json[.gz]`). Si un COD_RH o
57
+ un grupo está en más de una entrada, gana la primera.
58
+ - Escribe `unidos/gruplac/` y `unidos/cvlac/` con el mismo nombre y compresión de cada entrada, y
59
+ `unidos/cvgruplac_summary.json` con los conteos y los archivos que no pudo leer. `--only gruplac` escribe solo los
60
+ GrupLAC.
61
+ - Hace tres pasadas: lee los hashes de los CvLAC, une los GrupLAC y luego los CvLAC.
62
+
63
+ En Python:
64
+
65
+ ```python
66
+ from cvgruplac_json import link, link_gruplac, link_cvlac, id_hash
67
+
68
+ index = link(gruplacs, cvlacs) # listas de dicts de gruplac-json y cvlac-json, unidos en su lugar
69
+ ```
70
+
71
+ ## Los identificadores
72
+
73
+ Cada producto (obras, proyectos, eventos, patentes y cada ítem de `other_products`) empieza con dos claves:
74
+
75
+ ```json
76
+ "id": "efc8d0bb2fa50a617767aa814a57a7a40c1c956f",
77
+ "scrapping_ids": [
78
+ {"id": "efc8d0bb2fa50a617767aa814a57a7a40c1c956f", "source": "cvlac"},
79
+ {"id": "efc8d0bb2fa50a617767aa814a57a7a40c1c956f", "source": "gruplac"}
80
+ ]
81
+ ```
82
+
83
+ **En un GrupLAC**
84
+
85
+ - `source: "cvlac"`: una por integrante autor con `product_hash`, en el orden de los autores. Es el id del producto
86
+ en el CvLAC de ese integrante (el `product_hash` que cvlac-json pone en la entrada del dueño). Si el CvLAC del
87
+ integrante está entre las entradas, se pone solo si el producto está en él; si no está, se pone con
88
+ `"verified": false`. No hay ninguna en las secciones que solo tiene el GrupLAC
89
+ ([`data/sections.json`](src/cvgruplac_json/data/sections.json)), ni en proyectos y eventos, que el GrupLAC lista
90
+ sin autores.
91
+ - `source: "gruplac"`, siempre la última: el primer id de CvLAC si lo hay, así que un producto que está en los dos
92
+ tiene el mismo id. Si no, el SHA-1 de `título|código COL|tipo|año` (abajo).
93
+ - `id` es el de `source: "gruplac"`.
94
+
95
+ **En un CvLAC**
96
+
97
+ - `source: "cvlac"`: el `product_hash` del dueño. Los productos sin él (los eventos) usan el SHA-1 de
98
+ `título|COD_RH|event|año`.
99
+ - `source: "gruplac"`, con `group` (código COL): una por cada producto de un GrupLAC que lo lista como id de CvLAC
100
+ verificado. Un artículo del CV que está en dos grupos tiene dos.
101
+ - `id` es el de `source: "cvlac"` y reemplaza el `id` por posición de cvlac-json (`cvlac:COD_RH:n`).
102
+
103
+ **El hash.** `id_hash(título, dueño, tipo, año, n)` es la receta del `product_hash` con el dueño que corresponda: el
104
+ SHA-1 de
105
+
106
+ ```
107
+ <título>|<dueño>|<tipo>|<año>
108
+ ```
109
+
110
+ - título en minúsculas, sin tildes y solo con letras y dígitos; un ítem sin título usa su nombre (eventos) o su texto;
111
+ - dueño: el código COL del grupo (el número GrupLAC de 14 dígitos si no tiene), o el COD_RH con 10 dígitos (con un
112
+ COD_RH y los tipos `work`, `project`, `patent` y `other` da exactamente el `product_hash`);
113
+ - tipo: `work`, `project`, `event`, `patent` u `other`;
114
+ - año: `year_published`, `year`, el año de `start` o el de `date`.
115
+
116
+ Si varios productos del mismo JSON dan la misma clave, el n-ésimo, en el orden de la página, lleva `|n` al final; y
117
+ un candidato ya usado en ese JSON pasa al siguiente `n`. Por eso `id` es único dentro de cada GrupLAC y de cada
118
+ CvLAC. Ejemplo de clave de un evento: `juevesdelaciencia|COL0008423|event|2023`.
119
+
120
+ `product_hash.py` es una copia sin cambios del de cvlac-json y gruplac-json; `tests/product_hash_vectors.json` fija
121
+ su resultado en los tres paquetes.
122
+
123
+ ## Desarrollo
124
+
125
+ ```bash
126
+ python -m pip install -e ".[dev]"
127
+ python -m pytest
128
+ ruff check . && ruff format --check .
129
+ ```
130
+
131
+ Las versiones se publican en PyPI desde GitHub Actions al subir una etiqueta `v*`; ver
132
+ [PUBLISHING.md](PUBLISHING.md) y [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,106 @@
1
+ # cvgruplac-json
2
+
3
+ [![CI](https://github.com/restrepo/cvgruplac-json/actions/workflows/ci.yml/badge.svg)](https://github.com/restrepo/cvgruplac-json/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/cvgruplac-json.svg)](https://pypi.org/project/cvgruplac-json/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/cvgruplac-json.svg)](https://pypi.org/project/cvgruplac-json/)
6
+
7
+ Une los JSON de [cvlac-json](https://github.com/restrepo/cvlac-json) (el CvLAC de una persona) y de
8
+ [gruplac-json](https://github.com/restrepo/gruplac-json) (el GrupLAC de un grupo) de Minciencias: cada producto de
9
+ los dos recibe un `id` y una lista `scrapping_ids` con sus ids en una y otra fuente, así que un producto del grupo
10
+ apunta al mismo producto en el CV de cada integrante que lo firmó, y al revés.
11
+
12
+ No descarga nada ni llama a ninguna API: lee los JSON que ya escribieron los dos paquetes y escribe copias unidas,
13
+ sin tocar los originales. Solo usa la biblioteca estándar.
14
+
15
+ ## Instalación
16
+
17
+ ```bash
18
+ pip install cvgruplac-json
19
+ ```
20
+
21
+ ## Uso
22
+
23
+ ```bash
24
+ cvgruplac-json --cvlac cvlac/ --gruplac gruplac/ --out unidos/
25
+ cvgruplac-json --cvlac cvlac_udea/ cvlac_colombia/json/ --gruplac gruplac/json/ --out unidos/ --workers 4
26
+ cvgruplac-json --gruplac gruplac_COL0008423.json --cvlac cvlac_0000707619.json --out unidos/
27
+ cvgruplac-json --gruplac gruplac/ --out unidos/ # sin CvLAC: ids de CvLAC sin verificar
28
+ ```
29
+
30
+ - `--cvlac` y `--gruplac` reciben archivos o carpetas (`cvlac_*.json[.gz]`, `gruplac_*.json[.gz]`). Si un COD_RH o
31
+ un grupo está en más de una entrada, gana la primera.
32
+ - Escribe `unidos/gruplac/` y `unidos/cvlac/` con el mismo nombre y compresión de cada entrada, y
33
+ `unidos/cvgruplac_summary.json` con los conteos y los archivos que no pudo leer. `--only gruplac` escribe solo los
34
+ GrupLAC.
35
+ - Hace tres pasadas: lee los hashes de los CvLAC, une los GrupLAC y luego los CvLAC.
36
+
37
+ En Python:
38
+
39
+ ```python
40
+ from cvgruplac_json import link, link_gruplac, link_cvlac, id_hash
41
+
42
+ index = link(gruplacs, cvlacs) # listas de dicts de gruplac-json y cvlac-json, unidos en su lugar
43
+ ```
44
+
45
+ ## Los identificadores
46
+
47
+ Cada producto (obras, proyectos, eventos, patentes y cada ítem de `other_products`) empieza con dos claves:
48
+
49
+ ```json
50
+ "id": "efc8d0bb2fa50a617767aa814a57a7a40c1c956f",
51
+ "scrapping_ids": [
52
+ {"id": "efc8d0bb2fa50a617767aa814a57a7a40c1c956f", "source": "cvlac"},
53
+ {"id": "efc8d0bb2fa50a617767aa814a57a7a40c1c956f", "source": "gruplac"}
54
+ ]
55
+ ```
56
+
57
+ **En un GrupLAC**
58
+
59
+ - `source: "cvlac"`: una por integrante autor con `product_hash`, en el orden de los autores. Es el id del producto
60
+ en el CvLAC de ese integrante (el `product_hash` que cvlac-json pone en la entrada del dueño). Si el CvLAC del
61
+ integrante está entre las entradas, se pone solo si el producto está en él; si no está, se pone con
62
+ `"verified": false`. No hay ninguna en las secciones que solo tiene el GrupLAC
63
+ ([`data/sections.json`](src/cvgruplac_json/data/sections.json)), ni en proyectos y eventos, que el GrupLAC lista
64
+ sin autores.
65
+ - `source: "gruplac"`, siempre la última: el primer id de CvLAC si lo hay, así que un producto que está en los dos
66
+ tiene el mismo id. Si no, el SHA-1 de `título|código COL|tipo|año` (abajo).
67
+ - `id` es el de `source: "gruplac"`.
68
+
69
+ **En un CvLAC**
70
+
71
+ - `source: "cvlac"`: el `product_hash` del dueño. Los productos sin él (los eventos) usan el SHA-1 de
72
+ `título|COD_RH|event|año`.
73
+ - `source: "gruplac"`, con `group` (código COL): una por cada producto de un GrupLAC que lo lista como id de CvLAC
74
+ verificado. Un artículo del CV que está en dos grupos tiene dos.
75
+ - `id` es el de `source: "cvlac"` y reemplaza el `id` por posición de cvlac-json (`cvlac:COD_RH:n`).
76
+
77
+ **El hash.** `id_hash(título, dueño, tipo, año, n)` es la receta del `product_hash` con el dueño que corresponda: el
78
+ SHA-1 de
79
+
80
+ ```
81
+ <título>|<dueño>|<tipo>|<año>
82
+ ```
83
+
84
+ - título en minúsculas, sin tildes y solo con letras y dígitos; un ítem sin título usa su nombre (eventos) o su texto;
85
+ - dueño: el código COL del grupo (el número GrupLAC de 14 dígitos si no tiene), o el COD_RH con 10 dígitos (con un
86
+ COD_RH y los tipos `work`, `project`, `patent` y `other` da exactamente el `product_hash`);
87
+ - tipo: `work`, `project`, `event`, `patent` u `other`;
88
+ - año: `year_published`, `year`, el año de `start` o el de `date`.
89
+
90
+ Si varios productos del mismo JSON dan la misma clave, el n-ésimo, en el orden de la página, lleva `|n` al final; y
91
+ un candidato ya usado en ese JSON pasa al siguiente `n`. Por eso `id` es único dentro de cada GrupLAC y de cada
92
+ CvLAC. Ejemplo de clave de un evento: `juevesdelaciencia|COL0008423|event|2023`.
93
+
94
+ `product_hash.py` es una copia sin cambios del de cvlac-json y gruplac-json; `tests/product_hash_vectors.json` fija
95
+ su resultado en los tres paquetes.
96
+
97
+ ## Desarrollo
98
+
99
+ ```bash
100
+ python -m pip install -e ".[dev]"
101
+ python -m pytest
102
+ ruff check . && ruff format --check .
103
+ ```
104
+
105
+ Las versiones se publican en PyPI desde GitHub Actions al subir una etiqueta `v*`; ver
106
+ [PUBLISHING.md](PUBLISHING.md) y [CHANGELOG.md](CHANGELOG.md).
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24", "hatch-vcs>=0.4"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cvgruplac-json"
7
+ dynamic = ["version"]
8
+ description = "Link the JSONs of cvlac-json and gruplac-json: one id per product, in both directions."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Diego Restrepo", email = "restrepo@udea.edu.co" }]
14
+ keywords = []
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Environment :: Console",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff==0.17.3", "build>=1.2", "twine>=5"]
27
+
28
+ [project.scripts]
29
+ cvgruplac-json = "cvgruplac_json.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/restrepo/cvgruplac-json"
33
+ Repository = "https://github.com/restrepo/cvgruplac-json"
34
+ Issues = "https://github.com/restrepo/cvgruplac-json/issues"
35
+ Changelog = "https://github.com/restrepo/cvgruplac-json/blob/main/CHANGELOG.md"
36
+
37
+ # Version from the git tag (v0.1.0 -> 0.1.0); commits after a tag get 0.1.1.devN
38
+ [tool.hatch.version]
39
+ source = "vcs"
40
+
41
+ [tool.hatch.version.raw-options]
42
+ local_scheme = "no-local-version" # PyPI and TestPyPI reject local versions (+g1234abc)
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = ["src/", "tests/", "README.md", "CHANGELOG.md", "LICENSE"] # src/ includes data/sections.json
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/cvgruplac_json"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ addopts = "-ra --strict-markers"
53
+
54
+ [tool.coverage.run]
55
+ source = ["cvgruplac_json"]
56
+ branch = true
57
+
58
+ [tool.ruff]
59
+ line-length = 130
60
+ target-version = "py310"
61
+ src = ["src", "tests"]
62
+
63
+ [tool.ruff.lint]
64
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
65
+ ignore = [
66
+ "RUF001", "RUF002", "RUF003", # Spanish text with accents and typographic quotes (and product_hash.py, a verbatim copy)
67
+ ]
@@ -0,0 +1,13 @@
1
+ """Link the JSONs of cvlac-json and gruplac-json: one id per product, in both directions."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from .link import id_hash, id_key, link, link_cvlac, link_gruplac
6
+ from .product_hash import product_hash
7
+
8
+ try:
9
+ __version__ = version("cvgruplac-json")
10
+ except PackageNotFoundError: # running from a source tree that was never installed
11
+ __version__ = "0.0.0"
12
+
13
+ __all__ = ["__version__", "id_hash", "id_key", "link", "link_cvlac", "link_gruplac", "product_hash"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,158 @@
1
+ """Command line entry point: `cvgruplac-json`.
2
+
3
+ cvgruplac-json --cvlac CVLAC_DIR [...] --gruplac GRUPLAC_DIR [...] --out OUT_DIR [--workers N] [--only gruplac]
4
+
5
+ Inputs are folders (their cvlac_*.json[.gz] or gruplac_*.json[.gz] files) or single files. Three passes:
6
+ 1. read every CvLAC and keep its owner's product_hashes;
7
+ 2. link every GrupLAC (link_gruplac) and write it to OUT/gruplac/;
8
+ 3. link every CvLAC (link_cvlac) with the GrupLAC products found in pass 2 and write it to OUT/cvlac/.
9
+ Outputs keep the input's name and compression; inputs are not changed. OUT/cvgruplac_summary.json has the counts.
10
+ """
11
+
12
+ import argparse
13
+ import gzip
14
+ import json
15
+ import multiprocessing as mp
16
+ import os
17
+ import re
18
+ import sys
19
+ import time
20
+ from collections import Counter
21
+ from pathlib import Path
22
+
23
+ from . import __version__
24
+ from .link import link_cvlac, link_gruplac, owner_hashes
25
+
26
+ _STATE = {} # shared with forked workers: hashes, cods, index, out
27
+
28
+
29
+ def _files(paths, prefix):
30
+ """{key: path} for cvlac_<COD>.json[.gz] or gruplac_<CODE>.json[.gz]; the first path given wins."""
31
+ out = {}
32
+ for p in map(Path, paths):
33
+ cands = sorted(p.glob(f"{prefix}_*.json")) + sorted(p.glob(f"{prefix}_*.json.gz")) if p.is_dir() else [p]
34
+ for f in cands:
35
+ m = re.match(rf"{prefix}_(.+?)\.json(\.gz)?$", f.name)
36
+ out.setdefault(m.group(1) if m else f.name, f)
37
+ return out
38
+
39
+
40
+ def _read(path):
41
+ with (gzip.open if path.suffix == ".gz" else open)(path, "rt", encoding="utf-8") as f:
42
+ return json.load(f)
43
+
44
+
45
+ def _write(data, path):
46
+ tmp = path.with_name(path.name + ".tmp")
47
+ gz = {"compresslevel": 6} if path.suffix == ".gz" else {} # gzip's default 9 is several times slower
48
+ with (gzip.open if gz else open)(tmp, "wt", encoding="utf-8", **gz) as f:
49
+ json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
50
+ os.replace(tmp, path)
51
+
52
+
53
+ def _hashes(path):
54
+ try:
55
+ d = _read(path)
56
+ return d["cod_rh"], owner_hashes(d), None
57
+ except Exception as e:
58
+ return None, set(), f"{path}: {type(e).__name__}: {e}"
59
+
60
+
61
+ def _gruplac(path):
62
+ try:
63
+ d = _read(path)
64
+ matches = link_gruplac(d, _STATE["hashes"], _STATE["cods"])
65
+ d["cvgruplac"] = {"version": __version__, "cvlac_checked": _STATE["cods"] is not None}
66
+ _write(d, _STATE["out"] / "gruplac" / path.name)
67
+ c = Counter({k: v for k, v in d["counts"].items() if k.startswith(("products_", "cvlac_ids_"))})
68
+ return matches, c + Counter(gruplacs=1), None
69
+ except Exception as e:
70
+ return [], Counter(), f"{path}: {type(e).__name__}: {e}"
71
+
72
+
73
+ def _cvlac(path):
74
+ try:
75
+ d = _read(path)
76
+ link_cvlac(d, _STATE["index"])
77
+ d["cvgruplac"] = {"version": __version__}
78
+ _write(d, _STATE["out"] / "cvlac" / path.name)
79
+ return Counter(cvlac_products_in_gruplac=d["counts"]["products_in_gruplac"], cvlacs=1), None
80
+ except Exception as e:
81
+ return Counter(), f"{path}: {type(e).__name__}: {e}"
82
+
83
+
84
+ def _map(fn, items, workers, label, log):
85
+ """Results of fn over items, with forked workers when possible (they share _STATE without copying it)."""
86
+ t0, n = time.time(), len(items)
87
+ if workers > 1 and "fork" in mp.get_all_start_methods():
88
+ pool = mp.get_context("fork").Pool(workers)
89
+ it = pool.imap_unordered(fn, items, chunksize=16)
90
+ else:
91
+ pool, it = None, map(fn, items)
92
+ try:
93
+ for i, r in enumerate(it, 1):
94
+ if i % 5000 == 0 or i == n:
95
+ log(f"{label}: {i}/{n} ({time.time() - t0:.0f} s)")
96
+ yield r
97
+ finally:
98
+ if pool:
99
+ pool.close()
100
+ pool.join()
101
+
102
+
103
+ def run(cvlac, gruplac, out, workers=4, only=None, log=print):
104
+ """Link the files or folders given; returns the summary also written to OUT/cvgruplac_summary.json."""
105
+ out = Path(out)
106
+ cv, gr = _files(cvlac or [], "cvlac"), _files(gruplac or [], "gruplac")
107
+ log(f"{len(cv)} CvLAC and {len(gr)} GrupLAC files")
108
+ errors, cods, hashes = [], set(), set()
109
+ for cod, hs, err in _map(_hashes, list(cv.values()), workers, "1/3 CvLAC hashes", log):
110
+ if err:
111
+ errors.append(err)
112
+ else:
113
+ cods.add(cod)
114
+ hashes |= hs
115
+ _STATE.update(hashes=hashes, cods=cods if cv else None, out=out, index={})
116
+ (out / "gruplac").mkdir(parents=True, exist_ok=True)
117
+ total = Counter()
118
+ for matches, c, err in _map(_gruplac, list(gr.values()), workers, "2/3 GrupLAC", log):
119
+ errors += [err] if err else []
120
+ total += c
121
+ for h, group, gid in matches:
122
+ _STATE["index"].setdefault(h, []).append((group, gid))
123
+ if only != "gruplac" and cv:
124
+ (out / "cvlac").mkdir(parents=True, exist_ok=True)
125
+ for c, err in _map(_cvlac, list(cv.values()), workers, "3/3 CvLAC", log):
126
+ errors += [err] if err else []
127
+ total += c
128
+ summary = {"version": __version__, "cvlac_files": len(cv), "gruplac_files": len(gr), **total, "errors": errors}
129
+ out.mkdir(parents=True, exist_ok=True)
130
+ _write(summary, out / "cvgruplac_summary.json")
131
+ _STATE.clear()
132
+ return summary
133
+
134
+
135
+ def main(argv=None):
136
+ ap = argparse.ArgumentParser(
137
+ prog="cvgruplac-json",
138
+ description="Link the JSONs of cvlac-json and gruplac-json: one id per product, in both directions.",
139
+ epilog="Example: cvgruplac-json --cvlac cvlac_json/ --gruplac gruplac_json/ --out linked/",
140
+ )
141
+ ap.add_argument("--cvlac", nargs="+", default=[], metavar="PATH", help="cvlac-json files or folders")
142
+ ap.add_argument("--gruplac", nargs="+", default=[], metavar="PATH", help="gruplac-json files or folders")
143
+ ap.add_argument("--out", required=True, help="output folder (gruplac/, cvlac/, cvgruplac_summary.json)")
144
+ ap.add_argument("--workers", type=int, default=min(4, os.cpu_count() or 1), help="processes (default: up to 4)")
145
+ ap.add_argument("--only", choices=["gruplac"], help="write only the linked GrupLACs")
146
+ ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
147
+ a = ap.parse_args(argv)
148
+ if not a.gruplac and not a.cvlac:
149
+ ap.error("give --gruplac and/or --cvlac")
150
+ s = run(a.cvlac, a.gruplac, a.out, a.workers, a.only, log=lambda m: print(m, file=sys.stderr))
151
+ print(json.dumps({k: v for k, v in s.items() if k != "errors"} | {"errors": len(s["errors"])}, indent=1))
152
+ for e in s["errors"][:20]:
153
+ print(e, file=sys.stderr)
154
+ return 1 if s["errors"] else 0
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())