lr-gladiator 0.6.0__py3-none-any.whl → 0.7.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
@@ -156,21 +156,88 @@ class ArenaClient:
156
156
  out_dir.mkdir(parents=True, exist_ok=True)
157
157
  downloaded: List[Path] = []
158
158
  for f in files:
159
+ # Skip associations with no blob
160
+ if not f.get("haveContent", True):
161
+ self._log(
162
+ f"Skip {item_number}: file {f.get('filename')} has no content"
163
+ )
164
+ continue
165
+
159
166
  url = f.get("downloadUrl") or f.get("url")
160
167
  filename = f.get("filename") or f.get("name")
161
168
  if not url or not filename:
162
169
  continue
170
+
163
171
  p = out_dir / filename
164
- with self.session.get(
165
- url,
166
- stream=True,
167
- headers={"arena_session_id": self.cfg.arena_session_id or ""},
168
- ) as r:
169
- r.raise_for_status()
170
- with open(p, "wb") as fh:
171
- for chunk in r.iter_content(128 * 1024):
172
- fh.write(chunk)
173
- downloaded.append(p)
172
+ try:
173
+ with self.session.get(
174
+ url,
175
+ stream=True,
176
+ headers={"arena_session_id": self.cfg.arena_session_id or ""},
177
+ ) as r:
178
+ # If the blob is missing/forbidden, don’t abort the whole command
179
+ if r.status_code in (400, 403, 404):
180
+ self._log(
181
+ f"Skip {item_number}: {filename} content unavailable "
182
+ f"(HTTP {r.status_code})"
183
+ )
184
+ continue
185
+ r.raise_for_status()
186
+ with open(p, "wb") as fh:
187
+ for chunk in r.iter_content(128 * 1024):
188
+ fh.write(chunk)
189
+ downloaded.append(p)
190
+ except requests.HTTPError as e:
191
+ # Be resilient: log and continue
192
+ self._log(f"Download failed for {filename}: {e}")
193
+ continue
194
+ return downloaded
195
+
196
+ def download_files_recursive(
197
+ self,
198
+ item_number: str,
199
+ revision: Optional[str] = None,
200
+ out_dir: Path = Path("."),
201
+ *,
202
+ max_depth: Optional[int] = None,
203
+ ) -> List[Path]:
204
+ """
205
+ Download files for `item_number` AND, recursively, for all subassemblies
206
+ discovered via the BOM. Each child item is placed under a subdirectory:
207
+ <out_dir>/<child_item_number>/
208
+ Root files go directly in <out_dir>/.
209
+
210
+ Depth semantics match `get_bom(..., recursive=True, max_depth=...)`.
211
+ """
212
+ # Ensure the root directory exists
213
+ out_dir.mkdir(parents=True, exist_ok=True)
214
+
215
+ downloaded: List[Path] = []
216
+ # 1) Download files for the root item into out_dir
217
+ downloaded.extend(self.download_files(item_number, revision, out_dir=out_dir))
218
+
219
+ # 2) Expand BOM recursively and collect unique child item numbers
220
+ lines = self.get_bom(
221
+ item_number,
222
+ revision,
223
+ recursive=True,
224
+ max_depth=max_depth,
225
+ )
226
+ seen_children = set()
227
+ for ln in lines:
228
+ child_num = (ln or {}).get("itemNumber")
229
+ if not child_num or child_num == item_number:
230
+ continue
231
+ if child_num in seen_children:
232
+ continue
233
+ seen_children.add(child_num)
234
+
235
+ # Place each child's files under <out_dir>/<child_num>/
236
+ child_dir = out_dir / child_num
237
+ downloaded.extend(
238
+ self.download_files(child_num, revision, out_dir=child_dir)
239
+ )
240
+
174
241
  return downloaded
175
242
 
176
243
  def upload_file_to_working(
@@ -406,18 +473,18 @@ class ArenaClient:
406
473
  file_guid = f.get("guid") or f.get("id")
407
474
  norm.append(
408
475
  {
409
- "id": row.get("guid") or row.get("id"), # association id
410
- "fileGuid": file_guid, # actual file id
476
+ "id": row.get("guid") or row.get("id"),
477
+ "fileGuid": file_guid,
411
478
  "name": f.get("name") or f.get("title"),
412
479
  "filename": f.get("name") or f.get("title"),
413
480
  "size": f.get("size"),
414
481
  "checksum": f.get("checksum") or f.get("md5"),
482
+ "haveContent": f.get("haveContent", True),
415
483
  "downloadUrl": (
416
484
  f"{self._api_base()}/files/{file_guid}/content"
417
485
  if file_guid
418
486
  else None
419
487
  ),
420
- # for “pick latest” helper:
421
488
  "version": f.get("version") or f.get("edition"),
422
489
  "updatedAt": f.get("lastModifiedDateTime")
423
490
  or f.get("lastModifiedDate")
@@ -505,6 +572,7 @@ class ArenaClient:
505
572
  "filename": f.get("name") or f.get("title"),
506
573
  "size": f.get("size"),
507
574
  "checksum": f.get("checksum") or f.get("md5"),
575
+ "haveContent": f.get("haveContent", True),
508
576
  "downloadUrl": (
509
577
  f"{self._api_base()}/files/{file_guid}/content"
510
578
  if file_guid
gladiator/cli.py CHANGED
@@ -221,10 +221,27 @@ def get_files(
221
221
  "--out",
222
222
  help="Output directory (default: a folder named after the item number)",
223
223
  ),
224
+ recursive: bool = typer.Option(
225
+ False,
226
+ "--recursive/--no-recursive",
227
+ help="Recursively download files from subassemblies",
228
+ ),
229
+ max_depth: Optional[int] = typer.Option(
230
+ None,
231
+ "--max-depth",
232
+ min=1,
233
+ help="Maximum recursion depth for --recursive (1 = only direct children).",
234
+ ),
224
235
  ):
225
236
  try:
226
- out_dir = out or Path(item)
227
- paths = _client().download_files(item, revision, out_dir=out_dir)
237
+ out_dir = out or Path(item) # article numbers are never files; safe as dir name
238
+ if recursive:
239
+ paths = _client().download_files_recursive(
240
+ item, revision, out_dir=out_dir, max_depth=max_depth
241
+ )
242
+ else:
243
+ paths = _client().download_files(item, revision, out_dir=out_dir)
244
+
228
245
  for p in paths:
229
246
  print(str(p))
230
247
  except requests.HTTPError as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lr-gladiator
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -0,0 +1,10 @@
1
+ gladiator/__init__.py,sha256=ZeHpVdzARFyIp9QbdTkX0jNqnbRFX5nFQ5RkEFzSRL0,208
2
+ gladiator/arena.py,sha256=x2dg-JImb9Lw1fIj0ptm48GF1m4zIj2pmg4GqmWBPbA,30285
3
+ gladiator/cli.py,sha256=Pi1yjRZDr2s4izq51xQgpUiKN-Z7wl7yQjHiht9J9BU,10295
4
+ gladiator/config.py,sha256=oe2UpFv1HcrP1-lVWs_nnex444Igq18BW3nTs9wL__k,1760
5
+ lr_gladiator-0.7.0.dist-info/licenses/LICENSE,sha256=2CEtbEagerjoU3EDSk-eTM5LKgI_RpiVIOh3_CV4kms,1069
6
+ lr_gladiator-0.7.0.dist-info/METADATA,sha256=RckucPmR2KG5YwcP9gq3yNf-21JYDhEtT_uAdnl32BM,1912
7
+ lr_gladiator-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ lr_gladiator-0.7.0.dist-info/entry_points.txt,sha256=SLka4w7iGS2B8HrbeZyNk5mxaIC6QKcv93us1OaWNwQ,48
9
+ lr_gladiator-0.7.0.dist-info/top_level.txt,sha256=tfrcAmK7_7Lf63w7kWy0wv_Qg9RrcFWGoins1-jGUF4,10
10
+ lr_gladiator-0.7.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- gladiator/__init__.py,sha256=ZeHpVdzARFyIp9QbdTkX0jNqnbRFX5nFQ5RkEFzSRL0,208
2
- gladiator/arena.py,sha256=fy26XTNbJnC8XdeALZYNrzUhfbncQGUAU_hNvB3onDs,27747
3
- gladiator/cli.py,sha256=B8zHulDQUldC-WQ37jhkCfyxEPV1KvY3FEsF-Af25Wk,9716
4
- gladiator/config.py,sha256=oe2UpFv1HcrP1-lVWs_nnex444Igq18BW3nTs9wL__k,1760
5
- lr_gladiator-0.6.0.dist-info/licenses/LICENSE,sha256=2CEtbEagerjoU3EDSk-eTM5LKgI_RpiVIOh3_CV4kms,1069
6
- lr_gladiator-0.6.0.dist-info/METADATA,sha256=ohbCWnqTS1baU_cZiMtyBiYiqOtDj8AlkclIPzzl0U0,1912
7
- lr_gladiator-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- lr_gladiator-0.6.0.dist-info/entry_points.txt,sha256=SLka4w7iGS2B8HrbeZyNk5mxaIC6QKcv93us1OaWNwQ,48
9
- lr_gladiator-0.6.0.dist-info/top_level.txt,sha256=tfrcAmK7_7Lf63w7kWy0wv_Qg9RrcFWGoins1-jGUF4,10
10
- lr_gladiator-0.6.0.dist-info/RECORD,,