lr-gladiator 0.8.0__py3-none-any.whl → 0.10.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.

Potentially problematic release.


This version of lr-gladiator might be problematic. Click here for more details.

gladiator/arena.py CHANGED
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from typing import Dict, List, Optional, Tuple
10
10
  import requests
11
11
  from .config import LoginConfig
12
+ from .checksums import sha256_file
12
13
 
13
14
 
14
15
  class ArenaError(RuntimeError):
@@ -252,7 +253,7 @@ class ArenaClient:
252
253
  description: Optional[str] = None,
253
254
  primary: bool = True,
254
255
  latest_edition_association: bool = True,
255
- edition: str = "1",
256
+ edition: str = None,
256
257
  ) -> Dict:
257
258
  """
258
259
  Update-if-exists-else-create semantics, matching the bash script:
@@ -476,16 +477,16 @@ class ArenaClient:
476
477
  "id": row.get("guid") or row.get("id"),
477
478
  "fileGuid": file_guid,
478
479
  "name": f.get("name") or f.get("title"),
480
+ "title": f.get("title"),
479
481
  "filename": f.get("name") or f.get("title"),
480
482
  "size": f.get("size"),
481
- "checksum": f.get("checksum") or f.get("md5"),
482
483
  "haveContent": f.get("haveContent", True),
483
484
  "downloadUrl": (
484
485
  f"{self._api_base()}/files/{file_guid}/content"
485
486
  if file_guid
486
487
  else None
487
488
  ),
488
- "version": f.get("version") or f.get("edition"),
489
+ "edition": f.get("edition"),
489
490
  "updatedAt": f.get("lastModifiedDateTime")
490
491
  or f.get("lastModifiedDate")
491
492
  or f.get("creationDateTime"),
@@ -571,14 +572,13 @@ class ArenaClient:
571
572
  "name": f.get("name") or f.get("title"),
572
573
  "filename": f.get("name") or f.get("title"),
573
574
  "size": f.get("size"),
574
- "checksum": f.get("checksum") or f.get("md5"),
575
575
  "haveContent": f.get("haveContent", True),
576
576
  "downloadUrl": (
577
577
  f"{self._api_base()}/files/{file_guid}/content"
578
578
  if file_guid
579
579
  else None
580
580
  ),
581
- "version": f.get("version") or f.get("edition"),
581
+ "edition": f.get("edition"),
582
582
  "updatedAt": f.get("lastModifiedDateTime")
583
583
  or f.get("lastModifiedDate")
584
584
  or f.get("creationDateTime"),
@@ -604,6 +604,13 @@ class ArenaClient:
604
604
  if not file_path.exists() or not file_path.is_file():
605
605
  raise ArenaError(f"File not found: {file_path}")
606
606
 
607
+ filename = file_path.name # Use truncated SHA256 hash if no edition is provided
608
+ if not edition:
609
+ # Arena seems to only accept 16 characters of edition information.
610
+ # The hex digest gives 16 hex × 4 bits = 64 bits of entropy.
611
+ # Less than a million files, collision risk is practically zero (~1 / 10^8).
612
+ edition = sha256_file(file_path)[:16]
613
+
607
614
  # 0) Resolve EFFECTIVE revision guid from item number
608
615
  effective_guid = self._api_resolve_item_guid(item_number)
609
616
 
@@ -674,6 +681,17 @@ class ArenaClient:
674
681
  if existing_ct is not None:
675
682
  self.session.headers["Content-Type"] = existing_ct
676
683
  ur.raise_for_status()
684
+
685
+ # Update the edition label on the File itself
686
+ try:
687
+ put_url = f"{self._api_base()}/files/{file_guid}"
688
+ self._log(f"PUT {put_url} (set edition={edition})")
689
+ pr = self.session.put(put_url, json={"edition": str(edition)})
690
+ pr.raise_for_status()
691
+ except requests.HTTPError as e:
692
+ # Don't fail the whole operation if the label update is rejected
693
+ self._log(f"Edition update failed for {file_guid}: {e}")
694
+
677
695
  # Many tenants return 201 with no JSON for content updates. Be flexible.
678
696
  data = self._try_json(ur)
679
697
  if data is None:
@@ -683,6 +701,7 @@ class ArenaClient:
683
701
  "status": ur.status_code,
684
702
  "fileGuid": file_guid,
685
703
  "location": ur.headers.get("Location"),
704
+ "edition": str(edition),
686
705
  }
687
706
  return data
688
707
 
@@ -706,7 +725,7 @@ class ArenaClient:
706
725
  )
707
726
 
708
727
  # 3) Prepare multipart (create association)
709
- title = title or file_path.stem
728
+ title = title or file_path.name
710
729
  file_format = file_format or (
711
730
  file_path.suffix[1:].lower() if file_path.suffix else "bin"
712
731
  )
@@ -754,6 +773,22 @@ class ArenaClient:
754
773
  # Normalize common fields we use elsewhere
755
774
  row = resp if isinstance(resp, dict) else {}
756
775
  f = row.get("file", {})
776
+
777
+ # Ensure the edition label is exactly what we asked for (some tenants ignore form edition)
778
+ try:
779
+ file_guid_created = (f or {}).get("guid")
780
+ if file_guid_created and str(edition):
781
+ put_url = f"{self._api_base()}/files/{file_guid_created}"
782
+ self._log(f"PUT {put_url} (set edition={edition})")
783
+ pr = self.session.put(put_url, json={"edition": str(edition)})
784
+ pr.raise_for_status()
785
+ # Update local 'f' edition if the PUT succeeded
786
+ f["edition"] = str(edition)
787
+ except requests.HTTPError as e:
788
+ self._log(
789
+ f"Edition update after create failed for {file_guid_created}: {e}"
790
+ )
791
+
757
792
  return {
758
793
  "associationGuid": row.get("guid"),
759
794
  "primary": row.get("primary"),
@@ -765,7 +800,7 @@ class ArenaClient:
765
800
  "size": f.get("size"),
766
801
  "format": f.get("format"),
767
802
  "category": (f.get("category") or {}).get("name"),
768
- "edition": f.get("edition"),
803
+ "edition": f.get("edition") or str(edition),
769
804
  "lastModifiedDateTime": f.get("lastModifiedDateTime"),
770
805
  },
771
806
  "downloadUrl": (
gladiator/checksums.py ADDED
@@ -0,0 +1,31 @@
1
+ #! /usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # src/gladiator/checksums.py
4
+ from __future__ import annotations
5
+ from pathlib import Path
6
+ import hashlib
7
+ import base64
8
+
9
+
10
+ def sha256_file(path: Path, chunk_size: int = 128 * 1024) -> str:
11
+ """
12
+ Return the lowercase hex SHA-256 of the file at `path`.
13
+ Streams the file in chunks to support large files.
14
+ """
15
+ h = hashlib.sha256()
16
+ with open(path, "rb") as f:
17
+ for chunk in iter(lambda: f.read(chunk_size), b""):
18
+ h.update(chunk)
19
+ return h.hexdigest()
20
+
21
+
22
+ def md5_base64_file(path: Path, chunk_size: int = 128 * 1024) -> str:
23
+ """
24
+ Return base64-encoded MD5 digest of the file at `path`,
25
+ suitable for the Content-MD5 header per RFC 1864.
26
+ """
27
+ h = hashlib.md5()
28
+ with open(path, "rb") as f:
29
+ for chunk in iter(lambda: f.read(chunk_size), b""):
30
+ h.update(chunk)
31
+ return base64.b64encode(h.digest()).decode("ascii")
gladiator/cli.py CHANGED
@@ -160,11 +160,9 @@ def list_files(
160
160
  table = Table(title=f"Files for {item} rev {revision or '(latest approved)'}")
161
161
  table.add_column("Name")
162
162
  table.add_column("Size", justify="right")
163
- table.add_column("Checksum")
163
+ table.add_column("Edition")
164
164
  for f in files:
165
- table.add_row(
166
- str(f.get("filename")), str(f.get("size")), str(f.get("checksum"))
167
- )
165
+ table.add_row(str(f.get("name")), str(f.get("size")), str(f.get("edition")))
168
166
  print(table)
169
167
  except requests.HTTPError as e:
170
168
  typer.secho(f"Arena request failed: {e}", fg=typer.colors.RED, err=True)
@@ -313,9 +311,9 @@ def upload_file(
313
311
  False, "--primary/--no-primary", help="Mark association as primary"
314
312
  ),
315
313
  edition: str = typer.Option(
316
- "1",
314
+ None,
317
315
  "--edition",
318
- help="Edition number when creating a new association (default: 1)",
316
+ help="Edition number when creating a new association (default: SHA256 checksum of file)",
319
317
  ),
320
318
  ):
321
319
  """If a file with the same filename exists: update its content (new edition).
@@ -339,7 +337,7 @@ def upload_file(
339
337
  raise typer.Exit(2)
340
338
  except ArenaError as e:
341
339
  typer.secho(str(e), fg=typer.colors.RED, err=True)
342
-
340
+ raise typer.Exit(2)
343
341
 
344
342
  if __name__ == "__main__":
345
343
  app()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-gladiator
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: CLI and Python client for Arena PLM (app.bom.com): login, get revisions, list/download attachments, and upload to working revisions.
5
5
  Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
6
6
  License: MIT
@@ -179,3 +179,5 @@ python -m build
179
179
 
180
180
  - **How does Gladiator handle authentication?**
181
181
  It performs a `/login` call and stores the resulting `arenaSessionId` for reuse. If it expires, re-run `gladiator login`.
182
+
183
+
@@ -0,0 +1,11 @@
1
+ gladiator/__init__.py,sha256=ZeHpVdzARFyIp9QbdTkX0jNqnbRFX5nFQ5RkEFzSRL0,208
2
+ gladiator/arena.py,sha256=q98dvu92zcRH1UjvllnH7KRBbwflpuw3LwhqJX5hAjg,31931
3
+ gladiator/checksums.py,sha256=5_3ra5E6itOPhWkb9MAR1ywuMtnVa_CxdcAe8t5x3PM,934
4
+ gladiator/cli.py,sha256=pKi-xrotaRbynSWoSjwtV2KN7LT9R2Mk-FymCUSfRE0,11681
5
+ gladiator/config.py,sha256=oe2UpFv1HcrP1-lVWs_nnex444Igq18BW3nTs9wL__k,1760
6
+ lr_gladiator-0.10.0.dist-info/licenses/LICENSE,sha256=2CEtbEagerjoU3EDSk-eTM5LKgI_RpiVIOh3_CV4kms,1069
7
+ lr_gladiator-0.10.0.dist-info/METADATA,sha256=h12V01uHW7qh77WeXMGIO_3_68Yk_n1uRxHXKyxTmv0,4433
8
+ lr_gladiator-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ lr_gladiator-0.10.0.dist-info/entry_points.txt,sha256=SLka4w7iGS2B8HrbeZyNk5mxaIC6QKcv93us1OaWNwQ,48
10
+ lr_gladiator-0.10.0.dist-info/top_level.txt,sha256=tfrcAmK7_7Lf63w7kWy0wv_Qg9RrcFWGoins1-jGUF4,10
11
+ lr_gladiator-0.10.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- gladiator/__init__.py,sha256=ZeHpVdzARFyIp9QbdTkX0jNqnbRFX5nFQ5RkEFzSRL0,208
2
- gladiator/arena.py,sha256=x2dg-JImb9Lw1fIj0ptm48GF1m4zIj2pmg4GqmWBPbA,30285
3
- gladiator/cli.py,sha256=vGpyxcVqDzCALt32h7vS3PFpw5B4721kL-ADWL6hIlA,11667
4
- gladiator/config.py,sha256=oe2UpFv1HcrP1-lVWs_nnex444Igq18BW3nTs9wL__k,1760
5
- lr_gladiator-0.8.0.dist-info/licenses/LICENSE,sha256=2CEtbEagerjoU3EDSk-eTM5LKgI_RpiVIOh3_CV4kms,1069
6
- lr_gladiator-0.8.0.dist-info/METADATA,sha256=MT_mo2LrDU2eExg-lgXHgnNVoIfRlxHFCRdJJVffYuU,4430
7
- lr_gladiator-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- lr_gladiator-0.8.0.dist-info/entry_points.txt,sha256=SLka4w7iGS2B8HrbeZyNk5mxaIC6QKcv93us1OaWNwQ,48
9
- lr_gladiator-0.8.0.dist-info/top_level.txt,sha256=tfrcAmK7_7Lf63w7kWy0wv_Qg9RrcFWGoins1-jGUF4,10
10
- lr_gladiator-0.8.0.dist-info/RECORD,,