citesync 0.1.0__py3-none-any.whl

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,408 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "codemeta_tag": "3.1",
4
+ "generator": {
5
+ "name": "codemeta-drift",
6
+ "version": "0.1.0"
7
+ },
8
+ "rows": [
9
+ {
10
+ "format": "cff-1.2.0",
11
+ "term": "name",
12
+ "path": "title",
13
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
14
+ },
15
+ {
16
+ "format": "cff-1.2.0",
17
+ "term": "version",
18
+ "path": "version",
19
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
20
+ },
21
+ {
22
+ "format": "cff-1.2.0",
23
+ "term": "author",
24
+ "path": "authors[]",
25
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
26
+ },
27
+ {
28
+ "format": "cff-1.2.0",
29
+ "term": "author.givenName",
30
+ "path": "authors[].given-names",
31
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
32
+ },
33
+ {
34
+ "format": "cff-1.2.0",
35
+ "term": "author.familyName",
36
+ "path": "authors[].family-names",
37
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
38
+ },
39
+ {
40
+ "format": "cff-1.2.0",
41
+ "term": "author.@id",
42
+ "path": "authors[].orcid",
43
+ "source": "curated:the cell lists person.orcid / identifiers / doi; a person's ORCID is orcid"
44
+ },
45
+ {
46
+ "format": "cff-1.2.0",
47
+ "term": "license",
48
+ "path": "license",
49
+ "source": "curated:the cell lists license / license-url; license-url holds a licence URL, not an SPDX id"
50
+ },
51
+ {
52
+ "format": "cff-1.2.0",
53
+ "term": "identifier",
54
+ "path": "doi | identifiers[type=doi].value",
55
+ "source": "curated:the cell lists identifiers / person.orcid / doi; the software DOI is doi, or the doi entry of identifiers"
56
+ },
57
+ {
58
+ "format": "cff-1.2.0",
59
+ "term": "codeRepository",
60
+ "path": "repository-code",
61
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
62
+ },
63
+ {
64
+ "format": "cff-1.2.0",
65
+ "term": "description",
66
+ "path": "abstract",
67
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
68
+ },
69
+ {
70
+ "format": "cff-1.2.0",
71
+ "term": "keywords",
72
+ "path": "keywords",
73
+ "source": "crosswalk:crosswalks/Citation File Format 1.2.0.csv@3.1"
74
+ },
75
+ {
76
+ "format": "codemeta",
77
+ "term": "name",
78
+ "path": "name",
79
+ "source": "curated:codemeta's own term"
80
+ },
81
+ {
82
+ "format": "codemeta",
83
+ "term": "version",
84
+ "path": "version",
85
+ "source": "curated:codemeta's own term"
86
+ },
87
+ {
88
+ "format": "codemeta",
89
+ "term": "author",
90
+ "path": "author[]",
91
+ "source": "curated:codemeta's own term"
92
+ },
93
+ {
94
+ "format": "codemeta",
95
+ "term": "author.givenName",
96
+ "path": "author[].givenName",
97
+ "source": "curated:codemeta's own term"
98
+ },
99
+ {
100
+ "format": "codemeta",
101
+ "term": "author.familyName",
102
+ "path": "author[].familyName",
103
+ "source": "curated:codemeta's own term"
104
+ },
105
+ {
106
+ "format": "codemeta",
107
+ "term": "author.@id",
108
+ "path": "author[].@id",
109
+ "source": "curated:codemeta's own term"
110
+ },
111
+ {
112
+ "format": "codemeta",
113
+ "term": "license",
114
+ "path": "license",
115
+ "source": "curated:codemeta's own term"
116
+ },
117
+ {
118
+ "format": "codemeta",
119
+ "term": "identifier",
120
+ "path": "identifier",
121
+ "source": "curated:codemeta's own term"
122
+ },
123
+ {
124
+ "format": "codemeta",
125
+ "term": "codeRepository",
126
+ "path": "codeRepository",
127
+ "source": "curated:codemeta's own term"
128
+ },
129
+ {
130
+ "format": "codemeta",
131
+ "term": "description",
132
+ "path": "description",
133
+ "source": "curated:codemeta's own term"
134
+ },
135
+ {
136
+ "format": "codemeta",
137
+ "term": "keywords",
138
+ "path": "keywords",
139
+ "source": "curated:codemeta's own term"
140
+ },
141
+ {
142
+ "format": "zenodo",
143
+ "term": "name",
144
+ "path": "title",
145
+ "source": "crosswalk:crosswalks/Zenodo.csv@3.1"
146
+ },
147
+ {
148
+ "format": "zenodo",
149
+ "term": "version",
150
+ "path": "version",
151
+ "source": "crosswalk:crosswalks/Zenodo.csv@3.1"
152
+ },
153
+ {
154
+ "format": "zenodo",
155
+ "term": "author",
156
+ "path": "creators[]",
157
+ "source": "crosswalk:crosswalks/Zenodo.csv@3.1"
158
+ },
159
+ {
160
+ "format": "zenodo",
161
+ "term": "author.givenName",
162
+ "path": "creators[].name",
163
+ "source": "curated:no cell; Zenodo keeps one name string per creator, written 'Family, Given'"
164
+ },
165
+ {
166
+ "format": "zenodo",
167
+ "term": "author.familyName",
168
+ "path": "creators[].name",
169
+ "source": "curated:no cell; Zenodo keeps one name string per creator, written 'Family, Given'"
170
+ },
171
+ {
172
+ "format": "zenodo",
173
+ "term": "author.@id",
174
+ "path": "creators[].orcid",
175
+ "source": "curated:the cell says ORCID; the .zenodo.json key is orcid"
176
+ },
177
+ {
178
+ "format": "zenodo",
179
+ "term": "license",
180
+ "path": "license",
181
+ "source": "crosswalk:crosswalks/Zenodo.csv@3.1"
182
+ },
183
+ {
184
+ "format": "zenodo",
185
+ "term": "identifier",
186
+ "path": "doi",
187
+ "source": "curated:the cell says id, the Zenodo record id; the DOI field is doi",
188
+ "comment": "The GitHub integration mints the DOI and normally leaves doi unset in .zenodo.json."
189
+ },
190
+ {
191
+ "format": "zenodo",
192
+ "term": "codeRepository",
193
+ "path": "related_identifiers[relation=isSupplementTo].identifier",
194
+ "source": "curated:the cell says related_identifiers.isSupplementTo, a relation, not a key; the URL is the identifier of the entry with that relation"
195
+ },
196
+ {
197
+ "format": "zenodo",
198
+ "term": "description",
199
+ "path": "description",
200
+ "source": "crosswalk:crosswalks/Zenodo.csv@3.1"
201
+ },
202
+ {
203
+ "format": "zenodo",
204
+ "term": "keywords",
205
+ "path": "keywords",
206
+ "source": "crosswalk:crosswalks/Zenodo.csv@3.1"
207
+ },
208
+ {
209
+ "format": "pep621",
210
+ "term": "name",
211
+ "path": "project.name",
212
+ "source": "curated:no PEP 621 crosswalk upstream"
213
+ },
214
+ {
215
+ "format": "pep621",
216
+ "term": "version",
217
+ "path": "project.version",
218
+ "source": "curated:no PEP 621 crosswalk upstream"
219
+ },
220
+ {
221
+ "format": "pep621",
222
+ "term": "author",
223
+ "path": "project.authors[]",
224
+ "source": "curated:no PEP 621 crosswalk upstream"
225
+ },
226
+ {
227
+ "format": "pep621",
228
+ "term": "author.givenName",
229
+ "path": "project.authors[].name",
230
+ "source": "curated:no PEP 621 crosswalk upstream; an author has one name string"
231
+ },
232
+ {
233
+ "format": "pep621",
234
+ "term": "author.familyName",
235
+ "path": "project.authors[].name",
236
+ "source": "curated:no PEP 621 crosswalk upstream; an author has one name string"
237
+ },
238
+ {
239
+ "format": "pep621",
240
+ "term": "author.@id",
241
+ "path": null,
242
+ "source": "curated:no PEP 621 crosswalk upstream; PEP 621 has no person identifier"
243
+ },
244
+ {
245
+ "format": "pep621",
246
+ "term": "license",
247
+ "path": "project.license",
248
+ "source": "curated:no PEP 621 crosswalk upstream",
249
+ "comment": "An SPDX expression string since PEP 639. The legacy {text = ...} or {file = ...} table is the same key in an older shape."
250
+ },
251
+ {
252
+ "format": "pep621",
253
+ "term": "identifier",
254
+ "path": null,
255
+ "source": "curated:no PEP 621 crosswalk upstream; PEP 621 has no DOI field"
256
+ },
257
+ {
258
+ "format": "pep621",
259
+ "term": "codeRepository",
260
+ "path": "project.urls.Source | project.urls.Repository | project.urls.Homepage",
261
+ "source": "curated:no PEP 621 crosswalk upstream"
262
+ },
263
+ {
264
+ "format": "pep621",
265
+ "term": "description",
266
+ "path": "project.description",
267
+ "source": "curated:no PEP 621 crosswalk upstream"
268
+ },
269
+ {
270
+ "format": "pep621",
271
+ "term": "keywords",
272
+ "path": "project.keywords",
273
+ "source": "curated:no PEP 621 crosswalk upstream"
274
+ },
275
+ {
276
+ "format": "cargo",
277
+ "term": "name",
278
+ "path": "package.name",
279
+ "source": "curated:the cell says package.authors; the crate name is package.name"
280
+ },
281
+ {
282
+ "format": "cargo",
283
+ "term": "version",
284
+ "path": "package.version",
285
+ "source": "crosswalk:crosswalks/Cargo.csv@3.1"
286
+ },
287
+ {
288
+ "format": "cargo",
289
+ "term": "author",
290
+ "path": "package.authors[]",
291
+ "source": "crosswalk:crosswalks/Cargo.csv@3.1"
292
+ },
293
+ {
294
+ "format": "cargo",
295
+ "term": "author.givenName",
296
+ "path": "package.authors[]",
297
+ "source": "curated:no cell; Cargo keeps one 'Name <email>' string per author"
298
+ },
299
+ {
300
+ "format": "cargo",
301
+ "term": "author.familyName",
302
+ "path": "package.authors[]",
303
+ "source": "curated:no cell; Cargo keeps one 'Name <email>' string per author"
304
+ },
305
+ {
306
+ "format": "cargo",
307
+ "term": "author.@id",
308
+ "path": null,
309
+ "source": "curated:no cell; Cargo has no person identifier"
310
+ },
311
+ {
312
+ "format": "cargo",
313
+ "term": "license",
314
+ "path": "package.license",
315
+ "source": "crosswalk:crosswalks/Cargo.csv@3.1"
316
+ },
317
+ {
318
+ "format": "cargo",
319
+ "term": "identifier",
320
+ "path": null,
321
+ "source": "curated:no cell; Cargo has no DOI field"
322
+ },
323
+ {
324
+ "format": "cargo",
325
+ "term": "codeRepository",
326
+ "path": "package.repository",
327
+ "source": "crosswalk:crosswalks/Cargo.csv@3.1"
328
+ },
329
+ {
330
+ "format": "cargo",
331
+ "term": "description",
332
+ "path": "package.description",
333
+ "source": "curated:no cell; Cargo has package.description"
334
+ },
335
+ {
336
+ "format": "cargo",
337
+ "term": "keywords",
338
+ "path": "package.keywords",
339
+ "source": "curated:the cell says package.keyword; Cargo's key is package.keywords"
340
+ },
341
+ {
342
+ "format": "npm",
343
+ "term": "name",
344
+ "path": "name",
345
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
346
+ },
347
+ {
348
+ "format": "npm",
349
+ "term": "version",
350
+ "path": "version",
351
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
352
+ },
353
+ {
354
+ "format": "npm",
355
+ "term": "author",
356
+ "path": "author",
357
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
358
+ },
359
+ {
360
+ "format": "npm",
361
+ "term": "author.givenName",
362
+ "path": "author.name",
363
+ "source": "curated:no cell; the crosswalk's person name row says author.name, one name string"
364
+ },
365
+ {
366
+ "format": "npm",
367
+ "term": "author.familyName",
368
+ "path": "author.name",
369
+ "source": "curated:no cell; the crosswalk's person name row says author.name, one name string"
370
+ },
371
+ {
372
+ "format": "npm",
373
+ "term": "author.@id",
374
+ "path": null,
375
+ "source": "curated:no cell; package.json has no person identifier"
376
+ },
377
+ {
378
+ "format": "npm",
379
+ "term": "license",
380
+ "path": "license",
381
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
382
+ },
383
+ {
384
+ "format": "npm",
385
+ "term": "identifier",
386
+ "path": null,
387
+ "source": "curated:the cell says name, the package name; package.json has no DOI field"
388
+ },
389
+ {
390
+ "format": "npm",
391
+ "term": "codeRepository",
392
+ "path": "repository",
393
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
394
+ },
395
+ {
396
+ "format": "npm",
397
+ "term": "description",
398
+ "path": "description",
399
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
400
+ },
401
+ {
402
+ "format": "npm",
403
+ "term": "keywords",
404
+ "path": "keywords",
405
+ "source": "crosswalk:crosswalks/NodeJS.csv@3.1"
406
+ }
407
+ ]
408
+ }
citesync/mapping.py ADDED
@@ -0,0 +1,118 @@
1
+ """The vendored field mapping, and the path grammar its rows use.
2
+
3
+ The mapping says where each CodeMeta term sits in each format. Its contract is
4
+ docs/mapping-export.md in codemeta-drift, and MAPPING_SOURCE names the
5
+ codemeta-drift release and commit it was copied from.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ from functools import cache
11
+ from importlib.resources import files
12
+ from typing import Any
13
+
14
+ from citesync.errors import CheckError
15
+
16
+ SCHEMA_MAJOR = "1"
17
+ FORMATS = ("cff-1.2.0", "codemeta", "zenodo", "pep621", "cargo", "npm")
18
+ TERMS = ("name", "version", "author", "license", "identifier", "codeRepository", "description", "keywords")
19
+ PERSON_TERMS = ("author.givenName", "author.familyName", "author.@id")
20
+
21
+ _STEP = re.compile(r"^(@?[A-Za-z_][A-Za-z0-9_-]*)(?:\[\]|\[(@?[A-Za-z_][A-Za-z0-9_-]*)=([A-Za-z0-9_.-]+)\])?$")
22
+
23
+ type Mapping = dict[tuple[str, str], str | None]
24
+ """(format, term) to path, or None when the format has no place for the term."""
25
+
26
+
27
+ @cache
28
+ def load_mapping() -> Mapping:
29
+ """Read the vendored mapping and check what citesync needs from it.
30
+
31
+ Raises:
32
+ CheckError: on an unknown schema major, a duplicate row, or a missing row
33
+ for a term citesync reads.
34
+ """
35
+ name = "mapping-local.json"
36
+ data = json.loads(files("citesync").joinpath(name).read_text(encoding="utf-8"))
37
+ return check_mapping(data, name)
38
+
39
+
40
+ def check_mapping(data: dict[str, Any], name: str) -> Mapping:
41
+ """Turn a parsed mapping export into a lookup table, refusing what citesync can't use."""
42
+ major = str(data.get("schema_version", "")).split(".")[0]
43
+ if major != SCHEMA_MAJOR:
44
+ msg = f"{name}: schema_version {data.get('schema_version')!r}, this build reads {SCHEMA_MAJOR}.x"
45
+ raise CheckError(msg)
46
+ rows: Mapping = {}
47
+ for row in data["rows"]:
48
+ key = (row["format"], row["term"])
49
+ if key in rows:
50
+ msg = f"{name}: two rows for {key[0]} {key[1]}"
51
+ raise CheckError(msg)
52
+ rows[key] = row["path"]
53
+ missing = [f"{fmt} {term}" for fmt in FORMATS for term in TERMS + PERSON_TERMS if (fmt, term) not in rows]
54
+ if missing:
55
+ msg = f"{name}: no row for {', '.join(missing)}"
56
+ raise CheckError(msg)
57
+ for (fmt, term), path in rows.items():
58
+ if path is not None and fmt in FORMATS:
59
+ _parse(path) # fail on load, not on the first file that uses the row
60
+ if term in PERSON_TERMS and path is not None and rows.get((fmt, "author")) is None:
61
+ msg = f"{name}: {fmt} {term} has a path but {fmt} author has none"
62
+ raise CheckError(msg)
63
+ return rows
64
+
65
+
66
+ def _parse(path: str) -> list[list[tuple[str, str | None, str | None, bool]]]:
67
+ """Split a path into alternatives of steps: (key, filter key, filter value, is a list step)."""
68
+ alternatives = []
69
+ for alternative in path.split(" | "):
70
+ steps = []
71
+ for step in alternative.split("."):
72
+ match = _STEP.match(step)
73
+ if not match:
74
+ msg = f"mapping path {path!r}: bad step {step!r}"
75
+ raise CheckError(msg)
76
+ steps.append((match[1], match[2], match[3], step.endswith("]")))
77
+ alternatives.append(steps)
78
+ return alternatives
79
+
80
+
81
+ def resolve(doc: Any, path: str) -> list[Any]: # noqa: ANN401 - parsed JSON, TOML or YAML
82
+ """Every value at ``path`` in ``doc``, from the first alternative that has any.
83
+
84
+ A plain step reads a key of a mapping; ``key[]`` reads each element of a list;
85
+ ``key[k=v]`` reads the elements whose ``k`` equals ``v``. An empty list means the
86
+ path does not exist in ``doc``.
87
+ """
88
+ for steps in _parse(path):
89
+ nodes = [doc]
90
+ for key, fkey, fvalue, is_list in steps:
91
+ found = [node[key] for node in nodes if isinstance(node, dict) and key in node]
92
+ if is_list:
93
+ elements = [el for value in found if isinstance(value, list) for el in value]
94
+ if fkey is not None:
95
+ elements = [el for el in elements if isinstance(el, dict) and str(el.get(fkey)) == fvalue]
96
+ found = elements
97
+ nodes = found
98
+ if nodes:
99
+ return nodes
100
+ return []
101
+
102
+
103
+ def relative(path: str, parent: str) -> str:
104
+ """A person sub-term path relative to its term's path: ``authors[].orcid`` under ``authors[]`` is ``orcid``.
105
+
106
+ Each alternative of ``path`` must start with ``parent``. The result is ``""``
107
+ when the sub-term is the element itself (Cargo's one ``Name <email>`` string).
108
+ """
109
+ parts = []
110
+ for alternative in path.split(" | "):
111
+ if alternative == parent:
112
+ parts.append("")
113
+ elif alternative.startswith(parent + "."):
114
+ parts.append(alternative.removeprefix(parent + "."))
115
+ else:
116
+ msg = f"mapping path {path!r} does not start with its term's path {parent!r}"
117
+ raise CheckError(msg)
118
+ return " | ".join(parts)
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "codemeta-drift mapping export",
4
+ "description": "Where each CodeMeta term sits in each metadata format, read from one CodeMeta release (mapping-<codemeta_tag>.json). A row says where a value is, never how it is spelled: SPDX URL or id, ORCID URL or bare iD, 'Family, Given' and DOI forms are the consumer's business. Fields this schema does not define are allowed, so a reader that knows an older minor accepts a newer file. Contract and compatibility rules: https://github.com/simonvanlierde/codemeta-drift/blob/main/docs/mapping-export.md.",
5
+ "type": "object",
6
+ "required": ["schema_version", "codemeta_tag", "generator", "rows"],
7
+ "properties": {
8
+ "schema_version": {
9
+ "description": "'MAJOR.MINOR' of this schema. A consumer reads the major before anything else and stops on one it does not know. A minor adds optional fields, formats or terms and never changes what an existing field means.",
10
+ "type": "string",
11
+ "pattern": "^1\\.(0|[1-9][0-9]*)$"
12
+ },
13
+ "codemeta_tag": {
14
+ "description": "The codemeta/codemeta tag the crosswalk rows were read from, such as '3.1'.",
15
+ "type": "string",
16
+ "minLength": 1
17
+ },
18
+ "generator": {
19
+ "description": "The tool that wrote the file and its version. A hand-written file says so in a local version label, such as '0.1.0+hand'.",
20
+ "type": "object",
21
+ "required": ["name", "version"],
22
+ "properties": {
23
+ "name": { "type": "string", "minLength": 1 },
24
+ "version": { "type": "string", "minLength": 1 }
25
+ }
26
+ },
27
+ "rows": {
28
+ "description": "One row per (format, term). A pair appears at most once; JSON Schema cannot say that, so consumers check it.",
29
+ "type": "array",
30
+ "items": { "$ref": "#/$defs/row" }
31
+ }
32
+ },
33
+ "$defs": {
34
+ "row": {
35
+ "type": "object",
36
+ "required": ["format", "term", "path", "source"],
37
+ "properties": {
38
+ "format": {
39
+ "description": "Stable format id: cff-1.2.0, codemeta, zenodo, pep621, cargo, npm. A versioned format puts its version in the id.",
40
+ "type": "string",
41
+ "pattern": "^[a-z][a-z0-9]*(-[a-z0-9.]+)*$"
42
+ },
43
+ "term": {
44
+ "description": "A CodeMeta term (name, version, author, license, identifier, codeRepository, description, keywords), or a person sub-term qualified by the term it sits under: author.givenName, author.familyName, author.@id (ORCID).",
45
+ "type": "string",
46
+ "pattern": "^[A-Za-z][A-Za-z0-9]*(\\.@?[A-Za-z][A-Za-z0-9]*)?$"
47
+ },
48
+ "path": {
49
+ "description": "Where the value sits in the format's file, or null when the format has no place for it. Grammar: steps joined by '.'; a step is a key, a key followed by '[]' (each element of a list), or a key followed by '[k=v]' (each element whose k equals v). A key may contain letters, digits, '_' and '-', and may start with '@'. ' | ' separates alternatives, read left to right: the first that exists wins. A sub-term path is absolute and starts with its parent term's path. Examples: package.keywords, authors[].given-names, identifiers[type=doi].value, project.urls.Source | project.urls.Repository.",
50
+ "type": ["string", "null"],
51
+ "pattern": "^@?[A-Za-z_][A-Za-z0-9_-]*(\\[\\]|\\[@?[A-Za-z_][A-Za-z0-9_-]*=[A-Za-z0-9_.-]+\\])?(\\.@?[A-Za-z_][A-Za-z0-9_-]*(\\[\\]|\\[@?[A-Za-z_][A-Za-z0-9_-]*=[A-Za-z0-9_.-]+\\])?)*( \\| @?[A-Za-z_][A-Za-z0-9_-]*(\\[\\]|\\[@?[A-Za-z_][A-Za-z0-9_-]*=[A-Za-z0-9_.-]+\\])?(\\.@?[A-Za-z_][A-Za-z0-9_-]*(\\[\\]|\\[@?[A-Za-z_][A-Za-z0-9_-]*=[A-Za-z0-9_.-]+\\])?)*)*$"
52
+ },
53
+ "source": {
54
+ "description": "Why the row says what it says. 'crosswalk:<file>@<tag>' when the path reads the cell of that CodeMeta crosswalk; 'curated:<reason>' when the row departs from the crosswalk or the crosswalk has no cell.",
55
+ "type": "string",
56
+ "pattern": "^(crosswalk:crosswalks/[^@]+\\.csv@[^@\\s]+|curated:\\S.*)$"
57
+ },
58
+ "see_also": {
59
+ "description": "Upstream issue or pull request that reports the crosswalk cell this row departs from.",
60
+ "type": "string",
61
+ "pattern": "^https://"
62
+ },
63
+ "comment": {
64
+ "description": "Free text for a human reader. Never machine-read.",
65
+ "type": "string"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
citesync/py.typed ADDED
File without changes