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.
- cvgruplac_json-0.1.0/.gitignore +17 -0
- cvgruplac_json-0.1.0/CHANGELOG.md +18 -0
- cvgruplac_json-0.1.0/LICENSE +21 -0
- cvgruplac_json-0.1.0/PKG-INFO +132 -0
- cvgruplac_json-0.1.0/README.md +106 -0
- cvgruplac_json-0.1.0/pyproject.toml +67 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/__init__.py +13 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/__main__.py +3 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/cli.py +158 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/data/sections.json +491 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/link.py +183 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/product_hash.py +68 -0
- cvgruplac_json-0.1.0/src/cvgruplac_json/py.typed +0 -0
- cvgruplac_json-0.1.0/tests/conftest.py +95 -0
- cvgruplac_json-0.1.0/tests/product_hash_vectors.json +119 -0
- cvgruplac_json-0.1.0/tests/test_cli.py +42 -0
- cvgruplac_json-0.1.0/tests/test_link.py +98 -0
- cvgruplac_json-0.1.0/tests/test_product_hash.py +18 -0
|
@@ -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
|
+
[](https://github.com/restrepo/cvgruplac-json/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/cvgruplac-json/)
|
|
31
|
+
[](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
|
+
[](https://github.com/restrepo/cvgruplac-json/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/cvgruplac-json/)
|
|
5
|
+
[](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,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())
|