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.
Files changed (30) hide show
  1. {lcpcli-0.2.8 → lcpcli-0.3.0}/.gitignore +1 -0
  2. {lcpcli-0.2.8 → lcpcli-0.3.0}/LICENSE.txt +1 -1
  3. lcpcli-0.3.0/PKG-INFO +110 -0
  4. lcpcli-0.3.0/README.md +81 -0
  5. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/__init__.py +1 -1
  6. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/builder.py +52 -13
  7. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/check_files.py +29 -5
  8. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/cli.py +37 -8
  9. lcpcli-0.3.0/lcpcli/conllu_builder.py +310 -0
  10. lcpcli-0.3.0/lcpcli/corpert.py +146 -0
  11. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/lcp_corpus_template.json +9 -1
  12. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/lcp_upload.py +137 -25
  13. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/lcpcli.py +33 -29
  14. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/utils.py +39 -1
  15. {lcpcli-0.2.8 → lcpcli-0.3.0}/pyproject.toml +1 -2
  16. lcpcli-0.2.8/PKG-INFO +0 -370
  17. lcpcli-0.2.8/README.md +0 -340
  18. lcpcli-0.2.8/lcpcli/corpert.py +0 -617
  19. lcpcli-0.2.8/lcpcli/parsers/__init__.py +0 -0
  20. lcpcli-0.2.8/lcpcli/parsers/_parser.py +0 -755
  21. lcpcli-0.2.8/lcpcli/parsers/conllu.py +0 -429
  22. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/__main__.py +0 -0
  23. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/doc.conllu +0 -0
  24. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/meta.json +0 -0
  25. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/namedentity.csv +0 -0
  26. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/input/shot.csv +0 -0
  27. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/free_video_corpus/output/media/bunny.mp4 +0 -0
  28. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/input/in.conllu +0 -0
  29. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/input/in.vert +0 -0
  30. {lcpcli-0.2.8 → lcpcli-0.3.0}/lcpcli/data/input/in_tei_spoken.xml +0 -0
@@ -2,6 +2,7 @@ __pycache__/
2
2
  lcpcli/__pycache__/
3
3
  tmp.py
4
4
  *.pyc
5
+ .env
5
6
  build/
6
7
  dist/
7
8
  dist/
@@ -1,4 +1,4 @@
1
- Copyright 2025 LiRI - UZH
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).
@@ -1,3 +1,3 @@
1
- __version__ = "0.2.8"
1
+ __version__ = "0.3.0"
2
2
 
3
3
  from .lcpcli import Lcpcli # noqa: F401
@@ -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
- source_a = next(
89
- ra for ra in relation_attrs if any(ra not in a._attributes for a in args)
90
- )
91
- target_a = next(ra for ra in relation_attrs if ra != source_a)
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+\s?$", unit)
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(f"Invalid filename: {filename}")
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 reported it as nullable"
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: