lcpcli 0.3.0__tar.gz → 0.3.2__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 (25) hide show
  1. {lcpcli-0.3.0 → lcpcli-0.3.2}/PKG-INFO +3 -2
  2. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/__init__.py +1 -1
  3. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/builder.py +74 -46
  4. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/check_files.py +20 -16
  5. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/conllu_builder.py +10 -13
  6. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/corpert.py +5 -3
  7. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/lcp_upload.py +301 -136
  8. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/lcpcli.py +0 -52
  9. lcpcli-0.3.2/lcpcli/utils.py +235 -0
  10. {lcpcli-0.3.0 → lcpcli-0.3.2}/pyproject.toml +2 -1
  11. lcpcli-0.3.0/lcpcli/utils.py +0 -807
  12. {lcpcli-0.3.0 → lcpcli-0.3.2}/.gitignore +0 -0
  13. {lcpcli-0.3.0 → lcpcli-0.3.2}/LICENSE.txt +0 -0
  14. {lcpcli-0.3.0 → lcpcli-0.3.2}/README.md +0 -0
  15. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/__main__.py +0 -0
  16. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/cli.py +0 -0
  17. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/free_video_corpus/input/doc.conllu +0 -0
  18. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/free_video_corpus/input/meta.json +0 -0
  19. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/free_video_corpus/input/namedentity.csv +0 -0
  20. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/free_video_corpus/input/shot.csv +0 -0
  21. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/free_video_corpus/output/media/bunny.mp4 +0 -0
  22. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/input/in.conllu +0 -0
  23. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/input/in.vert +0 -0
  24. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/input/in_tei_spoken.xml +0 -0
  25. {lcpcli-0.3.0 → lcpcli-0.3.2}/lcpcli/data/lcp_corpus_template.json +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: lcpcli
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Helper for converting CONLLU files and uploading the corpus to LiRI Corpus Platform (LCP)
5
5
  Project-URL: Homepage, https://github.com/liri-uzh/lcpcli
6
6
  Project-URL: Issues, https://github.com/liri-uzh/lcpcli/issues
@@ -22,6 +22,7 @@ Requires-Dist: pandas>=2.2.2
22
22
  Requires-Dist: py7zr>=0.20.5
23
23
  Requires-Dist: requests>=2.30.0
24
24
  Requires-Dist: tqdm>=4.65.0
25
+ Requires-Dist: tuspy==1.1.0
25
26
  Requires-Dist: types-requests>=2.30.0.0
26
27
  Requires-Dist: types-tqdm>=4.65.0.1
27
28
  Requires-Dist: xmltodict>=0.13
@@ -1,3 +1,3 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.3.2"
2
2
 
3
3
  from .lcpcli import Lcpcli # noqa: F401
@@ -18,6 +18,8 @@ ATYPES_LOOKUP = ("text", "dict", "labels")
18
18
  NAMEDATALEN = 63
19
19
  PATTERN_TXT = "(must start with a lower case, be at leat 2 characters long and only contain alpha-numerical characters)"
20
20
 
21
+ IS_NUM = re.compile(r"^[0-9]+(\.[0-9]+)?$")
22
+
21
23
 
22
24
  def meta_subattr(meta: dict, k: str, v: Any) -> dict:
23
25
  """
@@ -26,11 +28,7 @@ def meta_subattr(meta: dict, k: str, v: Any) -> dict:
26
28
  sub_attr = meta.setdefault(k, {})
27
29
  if isinstance(v, list):
28
30
  sub_attr["type"] = "labels"
29
- elif (
30
- isinstance(v, (int, float))
31
- or isinstance(v, str)
32
- and v.replace(".", "", 1).isdigit()
33
- ):
31
+ elif isinstance(v, (int, float)) or isinstance(v, str) and IS_NUM.match(v):
34
32
  sub_attr["type"] = "text" if sub_attr.get("type") == "text" else "number"
35
33
  elif isinstance(v, dict):
36
34
  sub_attr["type"] = "dict"
@@ -48,6 +46,7 @@ def get_layer_method(layer: "Layer"):
48
46
  corpus._layers.pop(layer._name, "")
49
47
  fname = f"{layer._name.lower()}.csv"
50
48
  corpus._files[fname].close()
49
+ os.unlink(corpus._files[fname].name)
51
50
  corpus._files.pop(fname)
52
51
  return GlobalAttribute(corpus, layer._name, args[0])
53
52
  largs = [a for a in args]
@@ -168,6 +167,7 @@ class Corpus:
168
167
  revision: int | float = 1,
169
168
  url: str = "placeholder",
170
169
  license: str | None = None,
170
+ tmp_dir: str | None = None,
171
171
  ):
172
172
  self._name = name
173
173
  self._document = document
@@ -185,10 +185,11 @@ class Corpus:
185
185
  self._url = url
186
186
  self._license = license
187
187
  self._upperFrameDocument = 0
188
+ self._tmp_dir = tmp_dir
188
189
 
189
190
  def _csv_writer(self, fn: str):
190
191
  tmp = tempfile.NamedTemporaryFile(
191
- "w+", encoding="utf-8", newline="\n", delete=False
192
+ "w+", encoding="utf-8", newline="\n", delete=False, dir=self._tmp_dir
192
193
  )
193
194
  self._files[fn] = tmp
194
195
  return csv.writer(tmp)
@@ -339,7 +340,7 @@ class Corpus:
339
340
  "corpusDescription": self._corpus_description,
340
341
  "date": self._date,
341
342
  "url": self._url,
342
- "revision": 1,
343
+ "revision": self._revision,
343
344
  },
344
345
  "firstClass": {
345
346
  "token": self._token,
@@ -387,7 +388,11 @@ class Corpus:
387
388
  if ais_global:
388
389
  aopts["isGlobal"] = True
389
390
  if aopts["type"] == "categorical" and not ais_global:
390
- aopts["values"] = [str(v) for v in mapping.lookups[aname] if v]
391
+ aopts["values"] = [
392
+ str(v)
393
+ for v in mapping.lookups[aname]
394
+ if v is not None and v != ""
395
+ ]
391
396
  elif aopts["type"] == "ref":
392
397
  aopts.pop("type")
393
398
  aopts.pop("nullable", "")
@@ -447,8 +452,7 @@ class Layer:
447
452
  if re.match(r"[A-Z]", name):
448
453
  corpus = self._corpus
449
454
  layer = corpus._add_layer(name)
450
- self._contains.append(layer)
451
- layer._parents.append(self)
455
+ self.add(layer)
452
456
  return get_layer_method(layer)
453
457
  return super().__getattribute__(name)
454
458
 
@@ -494,7 +498,47 @@ class Layer:
494
498
  ch.append(c)
495
499
  return ch
496
500
 
497
- def make(self):
501
+ def _update_parents_anchors(self):
502
+ """Update the anchors of all the parents (recursively)"""
503
+ if not self._made:
504
+ return
505
+ corpus = self._corpus
506
+ parents = self._parents
507
+ while parents:
508
+ current_parents = [*parents]
509
+ parents = []
510
+ for parent in current_parents:
511
+ parents += parent._parents
512
+ for anc_name, anchors in self._anchorings.items():
513
+ if anc_name not in parent._anchorings:
514
+ parent._anchorings[anc_name] = [*anchors]
515
+ parent_anchors = parent._anchorings[anc_name]
516
+ if anchors[0] < parent_anchors[0]:
517
+ parent_anchors[0] = anchors[0]
518
+ if anc_name == "time" and parent._name == corpus._document:
519
+ if corpus._upperFrameDocument < parent_anchors[0]:
520
+ parent_anchors[0] = corpus._upperFrameDocument
521
+ corpus._upperFrameDocument = parent_anchors[1]
522
+ if anc_name != "location":
523
+ if anchors[1] > parent_anchors[1]:
524
+ parent_anchors[1] = anchors[1]
525
+ continue
526
+ if anchors[1] < parent_anchors[1]:
527
+ parent_anchors[1] = anchors[1]
528
+ if anchors[2] > parent_anchors[2]:
529
+ parent_anchors[2] = anchors[2]
530
+ if anchors[3] > parent_anchors[3]:
531
+ parent_anchors[3] = anchors[3]
532
+
533
+ def clear(self):
534
+ if not self._made:
535
+ return
536
+ # Prepare for deletion: no pointers to other layers/global attributes
537
+ self._parents = []
538
+ self._contains = []
539
+ self._attributes = {}
540
+
541
+ def make(self, clear=False):
498
542
  if self._made:
499
543
  return
500
544
  corpus = self._corpus
@@ -528,35 +572,8 @@ class Layer:
528
572
  )
529
573
  self._anchorings["stream"] = [char_low, corpus._char_counter]
530
574
  elif self._contains:
531
- unset_anchorings = {a for a in ANCHORINGS if not self._anchorings.get(a)}
532
575
  for child in self._contains:
533
576
  child.make()
534
- if child._name not in mapping.contains:
535
- mapping.contains.append(child._name)
536
- # Anchorings
537
- for a in unset_anchorings:
538
- if a not in child._anchorings:
539
- continue
540
- child_a = child._anchorings[a]
541
- if a not in self._anchorings:
542
- self._anchorings[a] = [*child_a]
543
- self_a = self._anchorings[a]
544
- if child_a[0] < self_a[0]:
545
- self_a[0] = child_a[0]
546
- if a == "time" and self._name == corpus._document:
547
- if corpus._upperFrameDocument < self_a[0]:
548
- self_a[0] = corpus._upperFrameDocument
549
- corpus._upperFrameDocument = self_a[1]
550
- if a != "location":
551
- if child_a[1] > self_a[1]:
552
- self_a[1] = child_a[1]
553
- continue
554
- if child_a[1] < self_a[1]:
555
- self_a[1] = child_a[1]
556
- if child_a[2] > self_a[2]:
557
- self_a[2] = child_a[2]
558
- if child_a[3] > self_a[3]:
559
- self_a[3] = child_a[3]
560
577
  if is_segment:
561
578
  tokens = [
562
579
  ch._attributes.values()
@@ -594,13 +611,17 @@ class Layer:
594
611
  rows.append(v)
595
612
  # Add any new attribute to mapping
596
613
  for aname, attr in self._attributes.items():
614
+ atype = attr._type
597
615
  if aname in mapping.attributes:
598
- try:
599
- mapping.attributes[aname]["subtype"] = attr._subtype
600
- except:
601
- pass
616
+ mattr = mapping.attributes[aname]
617
+ if atype == "text" and mattr["type"] != atype:
618
+ mattr["type"] = "text"
619
+ else:
620
+ try:
621
+ mapping.attributes[aname]["subtype"] = attr._subtype
622
+ except:
623
+ pass
602
624
  continue
603
- atype = attr._type
604
625
  mapping.attributes[aname] = {
605
626
  "type": atype,
606
627
  "nullable": (
@@ -690,6 +711,9 @@ class Layer:
690
711
  rows.append("" if val == None else str(val))
691
712
  mapping.csvs["_main"].writerow(rows)
692
713
  self._made = True
714
+ self._update_parents_anchors()
715
+ if clear:
716
+ self.clear()
693
717
  return self
694
718
 
695
719
  def set_time(self, *args):
@@ -745,13 +769,17 @@ class Layer:
745
769
  return self
746
770
 
747
771
  def add(self, *layers: "Layer"):
748
- assert not self._contains or all(
749
- l._name == self._contains[0]._name for l in layers
750
- ), RuntimeError("All the children of a layer must be of the same type")
772
+ # assert not self._contains or all(
773
+ # l._name == self._contains[0]._name for l in layers
774
+ # ), RuntimeError("All the children of a layer must be of the same type")
751
775
  self._contains += layers
776
+ mapping = self._corpus._layers[self._name]
752
777
  for layer in layers:
778
+ if layer._name not in mapping.contains:
779
+ mapping.contains.append(layer._name)
753
780
  if self not in layer._parents:
754
781
  layer._parents.append(self)
782
+ layer._update_parents_anchors()
755
783
  return self
756
784
 
757
785
 
@@ -1,10 +1,10 @@
1
1
  import csv
2
2
  import json
3
3
  import os
4
+ import re
4
5
  import sys
5
6
 
6
7
  from jsonschema import validate
7
- from re import match, findall
8
8
  from typing import Callable
9
9
  from uuid import UUID
10
10
 
@@ -12,6 +12,8 @@ EXTENSIONS = (".csv", ".tsv")
12
12
  LOOKUP_TYPES = ("dict", "text")
13
13
  NAMEDATALEN = 63
14
14
 
15
+ IS_NUM = re.compile(r"^[0-9]+(\.[0-9]+)?$")
16
+
15
17
 
16
18
  def is_lookup(p: dict) -> bool:
17
19
  return p.get("type", "") in LOOKUP_TYPES or "ref" in p
@@ -86,16 +88,16 @@ class Checker:
86
88
  assert UUID(uuid, version=4), SyntaxError(f"Invalid UUID ({uuid})")
87
89
 
88
90
  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:
91
+ is_num = IS_NUM.match(value.strip())
92
+ is_float = is_num and "." in value
93
+ if is_float:
92
94
  raise TypeError(
93
95
  f"Number attribute {aname} appears to contain floating values ({value}); add 'subtype': 'float' to its properties."
94
96
  )
95
- assert is_digit, TypeError(f"Number attribute {aname} is ill-formed: {value}")
97
+ assert is_num, TypeError(f"Number attribute {aname} is ill-formed: {value}")
96
98
 
97
99
  def check_float(self, value: str, aname: str = "") -> None:
98
- is_numerical = value.strip().replace(".", "", 1).isdigit()
100
+ is_numerical = IS_NUM.match(value.strip())
99
101
  assert is_numerical, TypeError(
100
102
  f"Float number attribute {aname} is ill-formed: {value}"
101
103
  )
@@ -109,6 +111,7 @@ class Checker:
109
111
  )
110
112
 
111
113
  def check_dict(self, str_obj: str, subtyps: dict) -> None:
114
+ error = ""
112
115
  try:
113
116
  json_obj = json.loads(str_obj)
114
117
  assert isinstance(json_obj, dict), TypeError(
@@ -122,7 +125,7 @@ class Checker:
122
125
  assert (
123
126
  isinstance(v, (int, float))
124
127
  or isinstance(v, str)
125
- and v.replace(".", "", 1).isdigit()
128
+ and IS_NUM.match(v)
126
129
  ), TypeError(f"Sub-attribute {k} is not a number ({v})")
127
130
  elif typ in ("labels", "array"):
128
131
  assert isinstance(v, list), TypeError(
@@ -132,15 +135,16 @@ class Checker:
132
135
  assert isinstance(v, str), TypeError(
133
136
  f"Sub-attribute {k} is not a valid text value ({v})"
134
137
  )
135
- except:
138
+ except Exception as e:
136
139
  json_obj = None
140
+ error = str(e)
137
141
  assert isinstance(json_obj, dict), SyntaxError(
138
- f"Invalid syntax for dict entry ({str_obj})"
142
+ f"Invalid syntax for dict entry ({str_obj})\n{error}"
139
143
  )
140
144
  return None
141
145
 
142
146
  def check_labels(self, bits: str, nbit: int) -> None:
143
- assert match(r"^[01]*$", bits), ValueError(
147
+ assert re.match(r"^[01]*$", bits), ValueError(
144
148
  f"Labels column should be series of 0s and 1s, got '{bits}'"
145
149
  )
146
150
  assert len(bits) == nbit, ValueError(
@@ -151,25 +155,25 @@ class Checker:
151
155
  def check_ftsvector(self, vector: str) -> None:
152
156
  whole_pattern = r"^('\d+([^']|'')*':\d+(,\d+)*(\s|$))+$"
153
157
  simple_unit_pattern = r"('([^']|'')*':[^\s]+)(\s|$)"
154
- units = findall(simple_unit_pattern, vector)
158
+ units = re.findall(simple_unit_pattern, vector)
155
159
  for n, (unit, *_) in enumerate(units):
156
160
  assert unit.startswith("'"), SyntaxError(
157
161
  f"Each value in the tsvector must start with a single quote character ({unit} -- {n})"
158
162
  )
159
- assert match(r"'\d+", unit), SyntaxError(
163
+ assert re.match(r"'\d+", unit), SyntaxError(
160
164
  f"Each value in the tsvector must start with a single quote character followed by an integer index ({unit} -- {n})"
161
165
  )
162
- m = match(r"'\d+(.*)':\d+(,\d+)*\s?$", unit)
166
+ m = re.match(r"'\d+(.*)':\d+(,\d+)*\s?$", unit)
163
167
  assert m, SyntaxError(
164
168
  f"Each value in the tsvector must end with a single quote followed by a colon and an integer index ({unit} -- {n})"
165
169
  )
166
- assert match(whole_pattern, vector), SyntaxError(
170
+ assert re.match(whole_pattern, vector), SyntaxError(
167
171
  f"Invalid tsvector string ({vector})"
168
172
  )
169
173
  return None
170
174
 
171
175
  def check_range(self, range: str, name: str) -> None:
172
- m = match(r"\[(\d+),(\d+)\)", range)
176
+ m = re.match(r"\[(\d+),(\d+)\)", range)
173
177
  assert m, SyntaxError(f"Range '{name}' not in the right format: {range}")
174
178
  l, u = (m[1], m[2])
175
179
  try:
@@ -192,7 +196,7 @@ class Checker:
192
196
  return None
193
197
 
194
198
  def check_xy_box(self, xy_box: str, name: str) -> None:
195
- m = match(r"\((\d+),(\d+)\),\((\d+),(\d+)\)", xy_box)
199
+ m = re.match(r"\((\d+),(\d+)\),\((\d+),(\d+)\)", xy_box)
196
200
  assert m, SyntaxError(f"Range '{name}' not in the right format: {xy_box}")
197
201
  x1, y1, x2, y2 = (m[1], m[2], m[3], m[4])
198
202
  try:
@@ -19,6 +19,8 @@ CONLLU_COLUMNS = (
19
19
  "MISC",
20
20
  )
21
21
 
22
+ IS_NUM = re.compile(r"^[0-9]+(\.[0-9]+)?$")
23
+
22
24
 
23
25
  class LayerProxy:
24
26
  dummy_corpus = Corpus("dummy")
@@ -36,10 +38,8 @@ class LayerProxy:
36
38
  val = value
37
39
  if isinstance(value, str):
38
40
  val = value.strip()
39
- if val.isdigit():
40
- val = int(val)
41
- elif val.replace(".", "", 1).isdigit():
42
- val = float(val)
41
+ if IS_NUM.match(val):
42
+ val = float(val) if "." in val else int(val)
43
43
  self.attributes[name.strip()] = val
44
44
 
45
45
 
@@ -214,10 +214,8 @@ def create_corpus(
214
214
  kwargs: dict = {}
215
215
  for n, x in enumerate(column_names[2:]):
216
216
  kwargs[x] = rest[n]
217
- if rest[n].isdigit():
218
- kwargs[x] = int(rest[n])
219
- elif rest[n].replace(".", "", 1).isdigit():
220
- kwargs[x] = float(rest[n])
217
+ if IS_NUM.match(rest[n]):
218
+ kwargs[x] = float(rest[n]) if "." in rest[n] else int(rest[n])
221
219
  if "feats" in column_names:
222
220
  feats_idx = column_names.index("feats")
223
221
  feats_str = rest[feats_idx - 2]
@@ -234,7 +232,8 @@ def create_corpus(
234
232
  0 if kwargs["misc"].pop("SpaceAfter") == "No" else 1
235
233
  )
236
234
  token_proxy = TokenProxy()
237
- if "head" in kwargs:
235
+ head_val = kwargs.get("head", "")
236
+ if head_val and head_val != "_":
238
237
  token_proxy.head = str(int(kwargs.pop("head")))
239
238
  if "deprel" in kwargs:
240
239
  token_proxy.deprel = kwargs.pop("deprel")
@@ -251,11 +250,9 @@ def create_corpus(
251
250
  current_doc.entity.make()
252
251
  print("Corpus created")
253
252
  except Exception as e:
254
- raise RuntimeError(
255
- f"""Error when creating the corpus:
253
+ raise RuntimeError(f"""Error when creating the corpus:
256
254
  {str(e)}
257
- Are all input files valid CoNLL-U files?"""
258
- )
255
+ Are all input files valid CoNLL-U files?""")
259
256
  return c
260
257
 
261
258
 
@@ -4,7 +4,7 @@ import re
4
4
 
5
5
  from .cli import _parse_cmd_line
6
6
  from .conllu_builder import process_files
7
- from .utils import default_json, find_config_file, yes_no_input
7
+ from .utils import default_json, find_config_file, say_yes
8
8
 
9
9
  ERROR_MSG = """
10
10
  Unrecognized input format.
@@ -21,6 +21,7 @@ class Corpert:
21
21
  output=None,
22
22
  extension=None,
23
23
  combine=True,
24
+ force_yes=False,
24
25
  **kwargs,
25
26
  ):
26
27
  """
@@ -39,6 +40,7 @@ class Corpert:
39
40
  self._path = os.path.normpath(content)
40
41
  self._combine = combine
41
42
  self._on_disk = True
43
+ self._force_yes = force_yes
42
44
  if os.path.isfile(content):
43
45
  self._input_files.append(content)
44
46
  elif os.path.isdir(content):
@@ -75,7 +77,7 @@ class Corpert:
75
77
  print(
76
78
  f"The destination folder {self.output} contains some JSON and/or CSV files which this operation might overwrite. Do you want to proceed?"
77
79
  )
78
- if not yes_no_input():
80
+ if not say_yes(auto=self._force_yes):
79
81
  print("Aborting the conversion operation.")
80
82
  return
81
83
 
@@ -118,7 +120,7 @@ class Corpert:
118
120
  f"The input folder ({self._path}) contains both files with a CoNLL extension and files with a different extension."
119
121
  )
120
122
  print("Ignore the files with a non-CoNLL extension?")
121
- if yes_no_input():
123
+ if say_yes(auto=self._force_yes):
122
124
  doc_files = [
123
125
  f for f in doc_files if f.lower().endswith((".conll", ".conllu"))
124
126
  ]