lcpcli 0.2.8__tar.gz → 0.3.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.
- {lcpcli-0.2.8 → lcpcli-0.3.0}/.gitignore +1 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/LICENSE.txt +1 -1
- lcpcli-0.3.0/PKG-INFO +110 -0
- lcpcli-0.3.0/README.md +81 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/__init__.py +1 -1
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/builder.py +52 -13
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/check_files.py +29 -5
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/cli.py +37 -8
- lcpcli-0.3.0/lcpcli/conllu_builder.py +310 -0
- lcpcli-0.3.0/lcpcli/corpert.py +146 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/lcp_corpus_template.json +9 -1
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/lcp_upload.py +137 -25
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/lcpcli.py +33 -29
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/utils.py +39 -1
- {lcpcli-0.2.8 → lcpcli-0.3.0}/pyproject.toml +1 -2
- lcpcli-0.2.8/PKG-INFO +0 -370
- lcpcli-0.2.8/README.md +0 -340
- lcpcli-0.2.8/lcpcli/corpert.py +0 -617
- lcpcli-0.2.8/lcpcli/parsers/__init__.py +0 -0
- lcpcli-0.2.8/lcpcli/parsers/_parser.py +0 -755
- lcpcli-0.2.8/lcpcli/parsers/conllu.py +0 -429
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/__main__.py +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/doc.conllu +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/meta.json +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/namedentity.csv +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/shot.csv +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/output/media/bunny.mp4 +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/input/in.conllu +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/input/in.vert +0 -0
- {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/input/in_tei_spoken.xml +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright
|
|
1
|
+
Copyright 2026 LiRI - UZH
|
|
2
2
|
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
4
|
|
lcpcli-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lcpcli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Helper for converting CONLLU files and uploading the corpus to LiRI Corpus Platform (LCP)
|
|
5
|
+
Project-URL: Homepage, https://github.com/liri-uzh/lcpcli
|
|
6
|
+
Project-URL: Issues, https://github.com/liri-uzh/lcpcli/issues
|
|
7
|
+
Author-email: Danny McDonald <daniel.mcdonald@uzh.ch>, Igor Mustač <igor.mustac@uzh.ch>, Jeremy Zehr <jeremy.zehr@uzh.ch>, Jonathan Schaber <jeremy.schaber@uzh.ch>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Keywords: CONLL,TEI,VERT,corpora,corpus,linguistics
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: diskcache>=5.6.3
|
|
18
|
+
Requires-Dist: jsonpickle>=3.0
|
|
19
|
+
Requires-Dist: jsonschema>=4.21
|
|
20
|
+
Requires-Dist: lxml>=4.7.1
|
|
21
|
+
Requires-Dist: pandas>=2.2.2
|
|
22
|
+
Requires-Dist: py7zr>=0.20.5
|
|
23
|
+
Requires-Dist: requests>=2.30.0
|
|
24
|
+
Requires-Dist: tqdm>=4.65.0
|
|
25
|
+
Requires-Dist: types-requests>=2.30.0.0
|
|
26
|
+
Requires-Dist: types-tqdm>=4.65.0.1
|
|
27
|
+
Requires-Dist: xmltodict>=0.13
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# LCP CLI module
|
|
31
|
+
|
|
32
|
+
> Command-line tool for converting CONLLU files and uploading the corpus to LCP
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Make sure you have python 3.11+ with `pip` installed in your local environment, then run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install lcpcli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
**Examples:**
|
|
45
|
+
|
|
46
|
+
Conversion of a CoNLL-U (Plus) corpus:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
lcpcli -i ~/conll_ext/ -o ~/upload/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Data upload:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
lcpcli -c ~/upload/ -k $API_KEY -s $API_SECRET -p "my project" --live
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Including `--live` points the upload to the live instance of LCP. Leave it out if you want to add a corpus to an instance of LCP running on `localhost`.
|
|
59
|
+
|
|
60
|
+
**Help:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
lcpcli --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`lcpcli` can take a corpus of CoNLL-U (PLUS) files and import it to a collection created on LCP.
|
|
67
|
+
|
|
68
|
+
Besides the standard token-level CoNLL-U fields (`form`, `lemma`, `upos`, `xpos`, `feats`, `head`, `deprel`, `deps`) one can also provide document-, paragraph- and sentence-level annotations using comment lines in the files (see [the CoNLL-U Format section](#conll-u-format)).
|
|
69
|
+
|
|
70
|
+
### CoNLL-U Format
|
|
71
|
+
|
|
72
|
+
The CoNLL-U format is documented at: https://universaldependencies.org/format.html
|
|
73
|
+
|
|
74
|
+
The LCP CLI converter will treat all the comments that start with `# newdoc KEY = VALUE` as document-level attributes, and all the comments that start with `# newpar KEY = VALUE` as paragraph-level attributes. All other comment lines following the format `# key = value` will be treated sentence-level attributes.
|
|
75
|
+
|
|
76
|
+
The key-value pairs in the `FEATS` and `MISC` columns of a token line will be mapped to corresponding attributes in the LCP corpus. Additionally, if the `MISC` cell includes `SpaceAfter=Yes` or `SpaceAfter=No` (case senstive) the token will be represented with (respectively, without) a trailing space character in the database.
|
|
77
|
+
|
|
78
|
+
#### CoNLL-U Plus
|
|
79
|
+
|
|
80
|
+
CoNLL-U Plus is an extension to the CoNLLU-U format documented at: https://universaldependencies.org/ext-format.html
|
|
81
|
+
|
|
82
|
+
If your files start with a comment line of the form `# global.columns = ID FORM LEMMA UPOS XPOS FEATS HEAD DEPREL DEPS MISC`, `lcpcli` will treat them as CoNLL-U PLUS files and process the columns according to the names you set in that line.
|
|
83
|
+
|
|
84
|
+
### CoNLL-U conversion and upload
|
|
85
|
+
|
|
86
|
+
1. Create a directory in which you have all your properly-fromatted CoNLL-U files.
|
|
87
|
+
|
|
88
|
+
2. Visit an LCP instance (e.g. _catchphrase_) and create a new collection if you don't already have one where your corpus should go.
|
|
89
|
+
|
|
90
|
+
3. Retrieve the API key and secret for your project by clicking on the button that says: "Create API Key".
|
|
91
|
+
|
|
92
|
+
4. Once you have your API key and secret, you can start converting and uploading your corpus by running the following command:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
lcpcli -i $CONLLU_FOLDER -o $OUTPUT_FOLDER -k $API_KEY -s $API_SECRET -p $PROJECT_NAME --live
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `$CONLLU_FOLDER` should point to the folder that contains your CONLLU files
|
|
99
|
+
- `$OUTPUT_FOLDER` should point to *another* folder that will be used to store the converted files to be uploaded
|
|
100
|
+
- `$API_KEY` is the key you copied from your project on LCP (still visible when you visit the page)
|
|
101
|
+
- `$API_SECRET` is the secret you copied from your project on LCP (only visible upon API Key creation)
|
|
102
|
+
- `$PROJECT_NAME` is the name of the project exactly as displayed on LCP -- it is case-sensitive, and space characters should be escaped
|
|
103
|
+
|
|
104
|
+
### Other input formats, rich data
|
|
105
|
+
|
|
106
|
+
Previous versions of `lcpcli` defined procedures to include rich annotations in CoNLL-U files, including time-anchored media files, in combination with annex non-CoNLL-U files. These methods are no longer supported -- use an older version of `lcpcli` if you require those features.
|
|
107
|
+
|
|
108
|
+
`lcpcli` now ships with a Python module called `lcpcli.builder` that you can use to convert any input format. The default CoNLL-U converter included in `lcpcli` uses `lcpcli.builder` under the hood.
|
|
109
|
+
|
|
110
|
+
You can find a short tutorial on how to use the module [in BUILDER.md](BUILDER.md). Further information can be found in [the LCP documentation](https://lcp.linguistik.uzh.ch/manual/builder.html).
|
lcpcli-0.3.0/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# LCP CLI module
|
|
2
|
+
|
|
3
|
+
> Command-line tool for converting CONLLU files and uploading the corpus to LCP
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Make sure you have python 3.11+ with `pip` installed in your local environment, then run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install lcpcli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
**Examples:**
|
|
16
|
+
|
|
17
|
+
Conversion of a CoNLL-U (Plus) corpus:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
lcpcli -i ~/conll_ext/ -o ~/upload/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Data upload:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
lcpcli -c ~/upload/ -k $API_KEY -s $API_SECRET -p "my project" --live
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Including `--live` points the upload to the live instance of LCP. Leave it out if you want to add a corpus to an instance of LCP running on `localhost`.
|
|
30
|
+
|
|
31
|
+
**Help:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
lcpcli --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`lcpcli` can take a corpus of CoNLL-U (PLUS) files and import it to a collection created on LCP.
|
|
38
|
+
|
|
39
|
+
Besides the standard token-level CoNLL-U fields (`form`, `lemma`, `upos`, `xpos`, `feats`, `head`, `deprel`, `deps`) one can also provide document-, paragraph- and sentence-level annotations using comment lines in the files (see [the CoNLL-U Format section](#conll-u-format)).
|
|
40
|
+
|
|
41
|
+
### CoNLL-U Format
|
|
42
|
+
|
|
43
|
+
The CoNLL-U format is documented at: https://universaldependencies.org/format.html
|
|
44
|
+
|
|
45
|
+
The LCP CLI converter will treat all the comments that start with `# newdoc KEY = VALUE` as document-level attributes, and all the comments that start with `# newpar KEY = VALUE` as paragraph-level attributes. All other comment lines following the format `# key = value` will be treated sentence-level attributes.
|
|
46
|
+
|
|
47
|
+
The key-value pairs in the `FEATS` and `MISC` columns of a token line will be mapped to corresponding attributes in the LCP corpus. Additionally, if the `MISC` cell includes `SpaceAfter=Yes` or `SpaceAfter=No` (case senstive) the token will be represented with (respectively, without) a trailing space character in the database.
|
|
48
|
+
|
|
49
|
+
#### CoNLL-U Plus
|
|
50
|
+
|
|
51
|
+
CoNLL-U Plus is an extension to the CoNLLU-U format documented at: https://universaldependencies.org/ext-format.html
|
|
52
|
+
|
|
53
|
+
If your files start with a comment line of the form `# global.columns = ID FORM LEMMA UPOS XPOS FEATS HEAD DEPREL DEPS MISC`, `lcpcli` will treat them as CoNLL-U PLUS files and process the columns according to the names you set in that line.
|
|
54
|
+
|
|
55
|
+
### CoNLL-U conversion and upload
|
|
56
|
+
|
|
57
|
+
1. Create a directory in which you have all your properly-fromatted CoNLL-U files.
|
|
58
|
+
|
|
59
|
+
2. Visit an LCP instance (e.g. _catchphrase_) and create a new collection if you don't already have one where your corpus should go.
|
|
60
|
+
|
|
61
|
+
3. Retrieve the API key and secret for your project by clicking on the button that says: "Create API Key".
|
|
62
|
+
|
|
63
|
+
4. Once you have your API key and secret, you can start converting and uploading your corpus by running the following command:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
lcpcli -i $CONLLU_FOLDER -o $OUTPUT_FOLDER -k $API_KEY -s $API_SECRET -p $PROJECT_NAME --live
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `$CONLLU_FOLDER` should point to the folder that contains your CONLLU files
|
|
70
|
+
- `$OUTPUT_FOLDER` should point to *another* folder that will be used to store the converted files to be uploaded
|
|
71
|
+
- `$API_KEY` is the key you copied from your project on LCP (still visible when you visit the page)
|
|
72
|
+
- `$API_SECRET` is the secret you copied from your project on LCP (only visible upon API Key creation)
|
|
73
|
+
- `$PROJECT_NAME` is the name of the project exactly as displayed on LCP -- it is case-sensitive, and space characters should be escaped
|
|
74
|
+
|
|
75
|
+
### Other input formats, rich data
|
|
76
|
+
|
|
77
|
+
Previous versions of `lcpcli` defined procedures to include rich annotations in CoNLL-U files, including time-anchored media files, in combination with annex non-CoNLL-U files. These methods are no longer supported -- use an older version of `lcpcli` if you require those features.
|
|
78
|
+
|
|
79
|
+
`lcpcli` now ships with a Python module called `lcpcli.builder` that you can use to convert any input format. The default CoNLL-U converter included in `lcpcli` uses `lcpcli.builder` under the hood.
|
|
80
|
+
|
|
81
|
+
You can find a short tutorial on how to use the module [in BUILDER.md](BUILDER.md). Further information can be found in [the LCP documentation](https://lcp.linguistik.uzh.ch/manual/builder.html).
|
|
@@ -4,8 +4,8 @@ import csv
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
import tempfile
|
|
8
7
|
import shutil
|
|
8
|
+
import tempfile
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
from uuid import uuid4
|
|
@@ -16,9 +16,7 @@ ANCHORINGS = ("stream", "time", "location")
|
|
|
16
16
|
# ATYPES = ("text", "categorical", "number", "dict", "labels")
|
|
17
17
|
ATYPES_LOOKUP = ("text", "dict", "labels")
|
|
18
18
|
NAMEDATALEN = 63
|
|
19
|
-
PATTERN_TXT = (
|
|
20
|
-
"(must start with a lower case and only contain alpha-numerical characters)"
|
|
21
|
-
)
|
|
19
|
+
PATTERN_TXT = "(must start with a lower case, be at leat 2 characters long and only contain alpha-numerical characters)"
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
def meta_subattr(meta: dict, k: str, v: Any) -> dict:
|
|
@@ -53,11 +51,13 @@ def get_layer_method(layer: "Layer"):
|
|
|
53
51
|
corpus._files.pop(fname)
|
|
54
52
|
return GlobalAttribute(corpus, layer._name, args[0])
|
|
55
53
|
largs = [a for a in args]
|
|
56
|
-
if layer._name == corpus._token and isinstance(largs[0], str):
|
|
54
|
+
if layer._name == corpus._token and largs and isinstance(largs[0], str):
|
|
57
55
|
form = largs.pop(0)
|
|
58
56
|
layer.form = form
|
|
59
57
|
if len(largs) > 0:
|
|
60
|
-
assert all(isinstance(c, Layer) for c in largs), RuntimeError(
|
|
58
|
+
assert all(isinstance(c, Layer) for c in largs), RuntimeError(
|
|
59
|
+
"Tried to pass non-layers as arguments of a layer"
|
|
60
|
+
)
|
|
61
61
|
layer.add(*largs)
|
|
62
62
|
for aname, avalue in kwargs.items():
|
|
63
63
|
setattr(layer, aname, avalue)
|
|
@@ -85,10 +85,23 @@ def get_layer_method(layer: "Layer"):
|
|
|
85
85
|
a.make()
|
|
86
86
|
return
|
|
87
87
|
# source is the attribute that's missing in at least one layer
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
try:
|
|
89
|
+
source_a = next(
|
|
90
|
+
ra
|
|
91
|
+
for ra in relation_attrs
|
|
92
|
+
if any(ra not in a._attributes for a in args)
|
|
93
|
+
)
|
|
94
|
+
except:
|
|
95
|
+
# if can't identify a missing attribute,
|
|
96
|
+
# look for "head"/"source" or use the first one
|
|
97
|
+
source_a = next(
|
|
98
|
+
(ra for ra in relation_attrs if ra in ("head", "source")),
|
|
99
|
+
next(ra for ra in relation_attrs),
|
|
100
|
+
)
|
|
101
|
+
target_a = next((ra for ra in relation_attrs if ra != source_a), None)
|
|
102
|
+
if not target_a:
|
|
103
|
+
# no target: possibly a one-token sentence, return
|
|
104
|
+
return
|
|
92
105
|
# reference nested sets by target's id
|
|
93
106
|
nested_sets = {
|
|
94
107
|
a._attributes[target_a]._value._id: NestedSet(
|
|
@@ -246,7 +259,7 @@ class Corpus:
|
|
|
246
259
|
can_categorize = (
|
|
247
260
|
not (is_token and aname in ("form", "lemma"))
|
|
248
261
|
and len(lookup) <= 100
|
|
249
|
-
and all(len(v) < NAMEDATALEN for v in lookup)
|
|
262
|
+
and all(len(str(v)) < NAMEDATALEN for v in lookup)
|
|
250
263
|
)
|
|
251
264
|
if can_categorize:
|
|
252
265
|
texts_to_categorical[na] = aname
|
|
@@ -374,7 +387,7 @@ class Corpus:
|
|
|
374
387
|
if ais_global:
|
|
375
388
|
aopts["isGlobal"] = True
|
|
376
389
|
if aopts["type"] == "categorical" and not ais_global:
|
|
377
|
-
aopts["values"] = [v for v in mapping.lookups[aname] if v]
|
|
390
|
+
aopts["values"] = [str(v) for v in mapping.lookups[aname] if v]
|
|
378
391
|
elif aopts["type"] == "ref":
|
|
379
392
|
aopts.pop("type")
|
|
380
393
|
aopts.pop("nullable", "")
|
|
@@ -418,6 +431,16 @@ class Layer:
|
|
|
418
431
|
assert re.match(r"[a-z][a-zA-Z0-9_]+$", name), RuntimeError(
|
|
419
432
|
f"The attribute '{name}' on the layer {self._name} does not match the pattern {PATTERN_TXT}"
|
|
420
433
|
)
|
|
434
|
+
# Disallow linebreak in token string values because it messes with CSV's (in particular, FTS)
|
|
435
|
+
if (
|
|
436
|
+
self._name == self._corpus._token
|
|
437
|
+
and isinstance(value, str)
|
|
438
|
+
and ("\n" in value or "\r" in value)
|
|
439
|
+
):
|
|
440
|
+
print(
|
|
441
|
+
f"Warning: a token attribute contains a linebreak; this is not allowed, removing the linebreaks from the value {value}."
|
|
442
|
+
)
|
|
443
|
+
value = value.replace("\n", "").replace("\r", "")
|
|
421
444
|
Attribute(self, name, value)
|
|
422
445
|
|
|
423
446
|
def __getattribute__(self, name: str):
|
|
@@ -494,6 +517,9 @@ class Layer:
|
|
|
494
517
|
rows.append(doc_name)
|
|
495
518
|
rows.append(json.dumps(self._media))
|
|
496
519
|
if is_token:
|
|
520
|
+
assert "form" in self._attributes, RuntimeError(
|
|
521
|
+
"Tried to make a token with no form"
|
|
522
|
+
)
|
|
497
523
|
seg_parent = self._find_in_parents(corpus._segment)
|
|
498
524
|
rows.append(seg_parent._id)
|
|
499
525
|
char_low = corpus._char_counter
|
|
@@ -569,6 +595,10 @@ class Layer:
|
|
|
569
595
|
# Add any new attribute to mapping
|
|
570
596
|
for aname, attr in self._attributes.items():
|
|
571
597
|
if aname in mapping.attributes:
|
|
598
|
+
try:
|
|
599
|
+
mapping.attributes[aname]["subtype"] = attr._subtype
|
|
600
|
+
except:
|
|
601
|
+
pass
|
|
572
602
|
continue
|
|
573
603
|
atype = attr._type
|
|
574
604
|
mapping.attributes[aname] = {
|
|
@@ -577,6 +607,10 @@ class Layer:
|
|
|
577
607
|
True if mapping.counter > 1 else False
|
|
578
608
|
), # adding a new attribute
|
|
579
609
|
}
|
|
610
|
+
try:
|
|
611
|
+
mapping.attributes[aname]["subtype"] = attr._subtype
|
|
612
|
+
except:
|
|
613
|
+
pass
|
|
580
614
|
if atype == "ref":
|
|
581
615
|
mapping.attributes[aname]["ref"] = attr._ref.lower()
|
|
582
616
|
elif atype in ATYPES_LOOKUP and aname != "meta":
|
|
@@ -724,6 +758,8 @@ class Layer:
|
|
|
724
758
|
class Attribute:
|
|
725
759
|
def __init__(self, layer: Layer, name: str, value: Any = None):
|
|
726
760
|
self._name = name
|
|
761
|
+
if name not in layer._attributes:
|
|
762
|
+
layer._attributes[name] = self
|
|
727
763
|
self._value = value
|
|
728
764
|
self._layer = layer
|
|
729
765
|
self._ref = None
|
|
@@ -739,6 +775,10 @@ class Attribute:
|
|
|
739
775
|
self._value = json.dumps(sorted_dict(value))
|
|
740
776
|
elif isinstance(value, (int, float)):
|
|
741
777
|
atype = "number"
|
|
778
|
+
if isinstance(value, float):
|
|
779
|
+
self._subtype = "float"
|
|
780
|
+
# overwrite to ensure subtype is taken into consideration
|
|
781
|
+
layer._attributes[name] = self
|
|
742
782
|
elif isinstance(value, GlobalAttribute):
|
|
743
783
|
atype = "ref"
|
|
744
784
|
self._ref = value._name
|
|
@@ -747,7 +787,6 @@ class Attribute:
|
|
|
747
787
|
atype = "entity"
|
|
748
788
|
self._ref = value._name
|
|
749
789
|
self._type: str = atype
|
|
750
|
-
layer._attributes[name] = self
|
|
751
790
|
|
|
752
791
|
|
|
753
792
|
class GlobalAttribute:
|
|
@@ -68,6 +68,8 @@ class Checker:
|
|
|
68
68
|
subtyps = aprops
|
|
69
69
|
elif aprops.get("type") == "dict":
|
|
70
70
|
subtyps = {k: v.get("type") for k, v in aprops.get("keys", {}).items()}
|
|
71
|
+
if typ == "number" and aprops.get("subtype", "") == "float":
|
|
72
|
+
typ = "float"
|
|
71
73
|
ret[aname] = (acol, typ, subtyps)
|
|
72
74
|
return ret
|
|
73
75
|
|
|
@@ -83,6 +85,21 @@ class Checker:
|
|
|
83
85
|
def check_uuid(self, uuid: str) -> None:
|
|
84
86
|
assert UUID(uuid, version=4), SyntaxError(f"Invalid UUID ({uuid})")
|
|
85
87
|
|
|
88
|
+
def check_number(self, value: str, aname: str = "") -> None:
|
|
89
|
+
is_digit = value.strip().isdigit()
|
|
90
|
+
is_almost_digit = not is_digit and value.strip().replace(".", "", 1).isdigit()
|
|
91
|
+
if is_almost_digit:
|
|
92
|
+
raise TypeError(
|
|
93
|
+
f"Number attribute {aname} appears to contain floating values ({value}); add 'subtype': 'float' to its properties."
|
|
94
|
+
)
|
|
95
|
+
assert is_digit, TypeError(f"Number attribute {aname} is ill-formed: {value}")
|
|
96
|
+
|
|
97
|
+
def check_float(self, value: str, aname: str = "") -> None:
|
|
98
|
+
is_numerical = value.strip().replace(".", "", 1).isdigit()
|
|
99
|
+
assert is_numerical, TypeError(
|
|
100
|
+
f"Float number attribute {aname} is ill-formed: {value}"
|
|
101
|
+
)
|
|
102
|
+
|
|
86
103
|
def check_categorical(self, value: str, values: None | list[str]) -> None:
|
|
87
104
|
assert len(value.encode("utf-8")) <= NAMEDATALEN, ValueError(
|
|
88
105
|
f"Found a categorical value ('{value}') that exceeds the database's limit of {NAMEDATALEN} bytes on enum values"
|
|
@@ -132,7 +149,7 @@ class Checker:
|
|
|
132
149
|
return None
|
|
133
150
|
|
|
134
151
|
def check_ftsvector(self, vector: str) -> None:
|
|
135
|
-
whole_pattern = r"^('\d+([^']|'')*':\d+(\s|$))+$"
|
|
152
|
+
whole_pattern = r"^('\d+([^']|'')*':\d+(,\d+)*(\s|$))+$"
|
|
136
153
|
simple_unit_pattern = r"('([^']|'')*':[^\s]+)(\s|$)"
|
|
137
154
|
units = findall(simple_unit_pattern, vector)
|
|
138
155
|
for n, (unit, *_) in enumerate(units):
|
|
@@ -142,7 +159,7 @@ class Checker:
|
|
|
142
159
|
assert match(r"'\d+", unit), SyntaxError(
|
|
143
160
|
f"Each value in the tsvector must start with a single quote character followed by an integer index ({unit} -- {n})"
|
|
144
161
|
)
|
|
145
|
-
m = match(r"'\d+(.*)':\d
|
|
162
|
+
m = match(r"'\d+(.*)':\d+(,\d+)*\s?$", unit)
|
|
146
163
|
assert m, SyntaxError(
|
|
147
164
|
f"Each value in the tsvector must end with a single quote followed by a colon and an integer index ({unit} -- {n})"
|
|
148
165
|
)
|
|
@@ -387,8 +404,11 @@ class Checker:
|
|
|
387
404
|
"vector": "ftsvector",
|
|
388
405
|
}
|
|
389
406
|
elif "_" in no_ext:
|
|
390
|
-
lname, aname, *remainder = no_ext.split("_")
|
|
391
|
-
assert not remainder, SyntaxError(
|
|
407
|
+
# lname, aname, *remainder = no_ext.split("_")
|
|
408
|
+
# assert not remainder, SyntaxError(
|
|
409
|
+
# f"Invalid filename: {filename} (cannot contain more than one underscore character)"
|
|
410
|
+
# )
|
|
411
|
+
lname, aname = no_ext.split("_", 1)
|
|
392
412
|
props = next(
|
|
393
413
|
(v for k, v in layer.items() if k.lower() == lname.lower()), None
|
|
394
414
|
)
|
|
@@ -490,7 +510,7 @@ class Checker:
|
|
|
490
510
|
typ = columns[headers[n]]
|
|
491
511
|
if not col:
|
|
492
512
|
assert headers[n] in nullables, ValueError(
|
|
493
|
-
f"Found an empty value for column #{n+1} ({headers[n]}) on line {counter} in {filename} even though the configuration does not
|
|
513
|
+
f"Found an empty value for column #{n+1} ({headers[n]}) on line {counter} in {filename} even though the configuration does not report it as nullable"
|
|
494
514
|
)
|
|
495
515
|
continue
|
|
496
516
|
if typ == "int":
|
|
@@ -523,6 +543,10 @@ class Checker:
|
|
|
523
543
|
self.check_uuid(col)
|
|
524
544
|
elif typ == "ftsvector":
|
|
525
545
|
self.check_ftsvector(col)
|
|
546
|
+
elif typ == "number":
|
|
547
|
+
self.check_number(col, headers[n])
|
|
548
|
+
elif typ == "float":
|
|
549
|
+
self.check_float(col, headers[n])
|
|
526
550
|
elif typ == "categorical":
|
|
527
551
|
assert layer_name, NotImplementedError(
|
|
528
552
|
f"Attributes of type 'categorical' are only supported on layers ({filename})"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
5
4
|
BOOL_KWARGS: dict[str, Any]
|
|
@@ -24,12 +23,6 @@ def _parse_cmd_line():
|
|
|
24
23
|
parser.add_argument(
|
|
25
24
|
"-e", "--extension", type=str, help="Output format when output is a directory"
|
|
26
25
|
)
|
|
27
|
-
parser.add_argument(
|
|
28
|
-
"-f", "--filter", required=False, type=str, help="Path to a Python filter file"
|
|
29
|
-
)
|
|
30
|
-
parser.add_argument(
|
|
31
|
-
"-u", "--lua-filter", required=False, type=str, help="Path to a Lua filter file"
|
|
32
|
-
)
|
|
33
26
|
parser.add_argument(
|
|
34
27
|
"-x",
|
|
35
28
|
"--example",
|
|
@@ -87,7 +80,7 @@ def _parse_cmd_line():
|
|
|
87
80
|
"--escape",
|
|
88
81
|
type=str,
|
|
89
82
|
required=False,
|
|
90
|
-
help="The character used to escape a character in the uploaded files (default is backslash
|
|
83
|
+
help="The character used to escape a character in the uploaded files (default is backslash \\)",
|
|
91
84
|
)
|
|
92
85
|
parser.add_argument(
|
|
93
86
|
"-j",
|
|
@@ -121,6 +114,42 @@ def _parse_cmd_line():
|
|
|
121
114
|
help="Run the pre-import check without importing.",
|
|
122
115
|
**BOOL_KWARGS,
|
|
123
116
|
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--force-corpus-overwrite",
|
|
119
|
+
required=False,
|
|
120
|
+
default=False,
|
|
121
|
+
help="Will overwrite any corpus with the same name in the destination collection.",
|
|
122
|
+
**BOOL_KWARGS,
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--force-output-overwrite",
|
|
126
|
+
required=False,
|
|
127
|
+
default=False,
|
|
128
|
+
help="Will overwrite any file in the destination folder with the same names as the newly generated ones.",
|
|
129
|
+
**BOOL_KWARGS,
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--conll-only",
|
|
133
|
+
required=False,
|
|
134
|
+
default=False,
|
|
135
|
+
help="Used in conjunction with -i/-o, will ignore any file not ending in .conll/.conllu from the input directory.",
|
|
136
|
+
**BOOL_KWARGS,
|
|
137
|
+
)
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--skip-check",
|
|
140
|
+
required=False,
|
|
141
|
+
default=False,
|
|
142
|
+
help="Skip the local checks before uploading the corpus (the server may still run checks).",
|
|
143
|
+
**BOOL_KWARGS,
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"-v",
|
|
147
|
+
"--version",
|
|
148
|
+
required=False,
|
|
149
|
+
default=False,
|
|
150
|
+
help="The current version of LCPCLI",
|
|
151
|
+
**BOOL_KWARGS,
|
|
152
|
+
)
|
|
124
153
|
break
|
|
125
154
|
except Exception as e:
|
|
126
155
|
if "type" in BOOL_KWARGS:
|