lcpcli 0.3.0__tar.gz → 0.3.1__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.1}/PKG-INFO +2 -1
  2. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/__init__.py +1 -1
  3. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/builder.py +71 -45
  4. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/check_files.py +20 -16
  5. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/conllu_builder.py +10 -13
  6. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/corpert.py +5 -3
  7. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/lcp_upload.py +284 -129
  8. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/lcpcli.py +0 -52
  9. lcpcli-0.3.1/lcpcli/utils.py +235 -0
  10. {lcpcli-0.3.0 → lcpcli-0.3.1}/pyproject.toml +2 -1
  11. lcpcli-0.3.0/lcpcli/utils.py +0 -807
  12. {lcpcli-0.3.0 → lcpcli-0.3.1}/.gitignore +0 -0
  13. {lcpcli-0.3.0 → lcpcli-0.3.1}/LICENSE.txt +0 -0
  14. {lcpcli-0.3.0 → lcpcli-0.3.1}/README.md +0 -0
  15. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/__main__.py +0 -0
  16. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/cli.py +0 -0
  17. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/free_video_corpus/input/doc.conllu +0 -0
  18. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/free_video_corpus/input/meta.json +0 -0
  19. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/free_video_corpus/input/namedentity.csv +0 -0
  20. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/free_video_corpus/input/shot.csv +0 -0
  21. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/free_video_corpus/output/media/bunny.mp4 +0 -0
  22. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/input/in.conllu +0 -0
  23. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/input/in.vert +0 -0
  24. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/input/in_tei_spoken.xml +0 -0
  25. {lcpcli-0.3.0 → lcpcli-0.3.1}/lcpcli/data/lcp_corpus_template.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lcpcli
3
- Version: 0.3.0
3
+ Version: 0.3.1
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.1"
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]
@@ -339,7 +338,7 @@ class Corpus:
339
338
  "corpusDescription": self._corpus_description,
340
339
  "date": self._date,
341
340
  "url": self._url,
342
- "revision": 1,
341
+ "revision": self._revision,
343
342
  },
344
343
  "firstClass": {
345
344
  "token": self._token,
@@ -387,7 +386,11 @@ class Corpus:
387
386
  if ais_global:
388
387
  aopts["isGlobal"] = True
389
388
  if aopts["type"] == "categorical" and not ais_global:
390
- aopts["values"] = [str(v) for v in mapping.lookups[aname] if v]
389
+ aopts["values"] = [
390
+ str(v)
391
+ for v in mapping.lookups[aname]
392
+ if v is not None and v != ""
393
+ ]
391
394
  elif aopts["type"] == "ref":
392
395
  aopts.pop("type")
393
396
  aopts.pop("nullable", "")
@@ -447,8 +450,7 @@ class Layer:
447
450
  if re.match(r"[A-Z]", name):
448
451
  corpus = self._corpus
449
452
  layer = corpus._add_layer(name)
450
- self._contains.append(layer)
451
- layer._parents.append(self)
453
+ self.add(layer)
452
454
  return get_layer_method(layer)
453
455
  return super().__getattribute__(name)
454
456
 
@@ -494,7 +496,47 @@ class Layer:
494
496
  ch.append(c)
495
497
  return ch
496
498
 
497
- def make(self):
499
+ def _update_parents_anchors(self):
500
+ """Update the anchors of all the parents (recursively)"""
501
+ if not self._made:
502
+ return
503
+ corpus = self._corpus
504
+ parents = self._parents
505
+ while parents:
506
+ current_parents = [*parents]
507
+ parents = []
508
+ for parent in current_parents:
509
+ parents += parent._parents
510
+ for anc_name, anchors in self._anchorings.items():
511
+ if anc_name not in parent._anchorings:
512
+ parent._anchorings[anc_name] = [*anchors]
513
+ parent_anchors = parent._anchorings[anc_name]
514
+ if anchors[0] < parent_anchors[0]:
515
+ parent_anchors[0] = anchors[0]
516
+ if anc_name == "time" and parent._name == corpus._document:
517
+ if corpus._upperFrameDocument < parent_anchors[0]:
518
+ parent_anchors[0] = corpus._upperFrameDocument
519
+ corpus._upperFrameDocument = parent_anchors[1]
520
+ if anc_name != "location":
521
+ if anchors[1] > parent_anchors[1]:
522
+ parent_anchors[1] = anchors[1]
523
+ continue
524
+ if anchors[1] < parent_anchors[1]:
525
+ parent_anchors[1] = anchors[1]
526
+ if anchors[2] > parent_anchors[2]:
527
+ parent_anchors[2] = anchors[2]
528
+ if anchors[3] > parent_anchors[3]:
529
+ parent_anchors[3] = anchors[3]
530
+
531
+ def clear(self):
532
+ if not self._made:
533
+ return
534
+ # Prepare for deletion: no pointers to other layers/global attributes
535
+ self._parents = []
536
+ self._contains = []
537
+ self._attributes = {}
538
+
539
+ def make(self, clear=False):
498
540
  if self._made:
499
541
  return
500
542
  corpus = self._corpus
@@ -528,35 +570,8 @@ class Layer:
528
570
  )
529
571
  self._anchorings["stream"] = [char_low, corpus._char_counter]
530
572
  elif self._contains:
531
- unset_anchorings = {a for a in ANCHORINGS if not self._anchorings.get(a)}
532
573
  for child in self._contains:
533
574
  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
575
  if is_segment:
561
576
  tokens = [
562
577
  ch._attributes.values()
@@ -594,13 +609,17 @@ class Layer:
594
609
  rows.append(v)
595
610
  # Add any new attribute to mapping
596
611
  for aname, attr in self._attributes.items():
612
+ atype = attr._type
597
613
  if aname in mapping.attributes:
598
- try:
599
- mapping.attributes[aname]["subtype"] = attr._subtype
600
- except:
601
- pass
614
+ mattr = mapping.attributes[aname]
615
+ if atype == "text" and mattr["type"] != atype:
616
+ mattr["type"] = "text"
617
+ else:
618
+ try:
619
+ mapping.attributes[aname]["subtype"] = attr._subtype
620
+ except:
621
+ pass
602
622
  continue
603
- atype = attr._type
604
623
  mapping.attributes[aname] = {
605
624
  "type": atype,
606
625
  "nullable": (
@@ -690,6 +709,9 @@ class Layer:
690
709
  rows.append("" if val == None else str(val))
691
710
  mapping.csvs["_main"].writerow(rows)
692
711
  self._made = True
712
+ self._update_parents_anchors()
713
+ if clear:
714
+ self.clear()
693
715
  return self
694
716
 
695
717
  def set_time(self, *args):
@@ -745,13 +767,17 @@ class Layer:
745
767
  return self
746
768
 
747
769
  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")
770
+ # assert not self._contains or all(
771
+ # l._name == self._contains[0]._name for l in layers
772
+ # ), RuntimeError("All the children of a layer must be of the same type")
751
773
  self._contains += layers
774
+ mapping = self._corpus._layers[self._name]
752
775
  for layer in layers:
776
+ if layer._name not in mapping.contains:
777
+ mapping.contains.append(layer._name)
753
778
  if self not in layer._parents:
754
779
  layer._parents.append(self)
780
+ layer._update_parents_anchors()
755
781
  return self
756
782
 
757
783
 
@@ -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
  ]