jartify 0.2.2__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jartify
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: TUI and CLI for searching JFrog Artifactory artifacts (version, filename, sha256)
5
5
  Author-email: Jakob Holst <code@jakobholst.dk>
6
6
  License: MIT
@@ -134,6 +134,9 @@ jartify-locate --init
134
134
  # commits so Ctrl+C keeps everything already indexed):
135
135
  jartify-locate --init --status=progress
136
136
 
137
+ # Or one AQL per repo, printing the repo name as it goes (commits per repo):
138
+ jartify-locate --init --status=step
139
+
137
140
  # Then search locally:
138
141
  jartify-locate # list everything (respects --repo / --limit)
139
142
  jartify-locate '*.json' # glob
@@ -108,6 +108,9 @@ jartify-locate --init
108
108
  # commits so Ctrl+C keeps everything already indexed):
109
109
  jartify-locate --init --status=progress
110
110
 
111
+ # Or one AQL per repo, printing the repo name as it goes (commits per repo):
112
+ jartify-locate --init --status=step
113
+
111
114
  # Then search locally:
112
115
  jartify-locate # list everything (respects --repo / --limit)
113
116
  jartify-locate '*.json' # glob
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jartify"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "TUI and CLI for searching JFrog Artifactory artifacts (version, filename, sha256)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.4"
@@ -19,6 +19,8 @@ from urllib.parse import urljoin
19
19
 
20
20
  import requests
21
21
  from packaging.utils import parse_sdist_filename, parse_wheel_filename
22
+ from requests.adapters import HTTPAdapter
23
+ from urllib3.util.retry import Retry
22
24
 
23
25
 
24
26
  @dataclass
@@ -85,6 +87,22 @@ class ArtifactoryClient:
85
87
  self.base_url = self.base_url + "/artifactory"
86
88
  self.timeout = timeout
87
89
  self.session = session or requests.Session()
90
+ # Transparently retry on connection drops (RemoteDisconnected, reset
91
+ # keep-alives) and transient 5xx. Exponential backoff: 0, 1, 2, 4, 8s.
92
+ _retry = Retry(
93
+ total=5,
94
+ connect=5,
95
+ read=5,
96
+ status=5,
97
+ backoff_factor=1.0,
98
+ status_forcelist=(502, 503, 504),
99
+ allowed_methods=frozenset(["GET", "POST", "HEAD"]),
100
+ raise_on_status=False,
101
+ respect_retry_after_header=True,
102
+ )
103
+ _adapter = HTTPAdapter(max_retries=_retry)
104
+ self.session.mount("http://", _adapter)
105
+ self.session.mount("https://", _adapter)
88
106
  self.session.verify = verify
89
107
  if not verify:
90
108
  # One-shot: silence the per-request InsecureRequestWarning.
@@ -287,6 +287,76 @@ def _init_paginated(
287
287
  return total_appended
288
288
 
289
289
 
290
+ def _init_step(
291
+ client: ArtifactoryClient, repos: list[str] | None
292
+ ) -> int:
293
+ """One AQL request per repo, printing the repo name as it goes.
294
+
295
+ Session-level retries handle transient connection drops. A per-repo
296
+ try/except catches anything that still fails after retries so one
297
+ bad repo doesn't abort the remaining ones; failures are listed at
298
+ the end.
299
+ """
300
+ if repos is None:
301
+ all_repos = client.list_repositories()
302
+ repo_keys = [r.get("key") for r in all_repos if r.get("key")]
303
+ if not repo_keys:
304
+ print(" no repos visible to this token.")
305
+ return 0
306
+ else:
307
+ repo_keys = list(repos)
308
+
309
+ conn = _db_connect()
310
+ t0 = time.time()
311
+ total_appended = 0
312
+ skipped = 0
313
+ failed: list[tuple[str, str]] = []
314
+ width = len(str(len(repo_keys)))
315
+ try:
316
+ for i, repo in enumerate(repo_keys, 1):
317
+ prefix = f"[{i:>{width}}/{len(repo_keys)}]"
318
+ indent = " " * (len(prefix) + 1)
319
+ print(f"{prefix} {repo} …", flush=True)
320
+ try:
321
+ items = client.aql(_aql_query([repo]))
322
+ artifact_rows, location_rows, skip = _build_rows(items)
323
+ with conn:
324
+ _insert_batch(conn, artifact_rows, location_rows)
325
+ total_appended += len(artifact_rows)
326
+ skipped += skip
327
+ print(
328
+ f"{indent}{len(artifact_rows):,} files appended "
329
+ f"({len(items):,} received)",
330
+ flush=True,
331
+ )
332
+ except Exception as exc:
333
+ # One repo died — remember and move on. The commit on
334
+ # previous repos has already landed thanks to per-repo
335
+ # transactions.
336
+ summary = f"{type(exc).__name__}: {exc}"
337
+ failed.append((repo, summary))
338
+ print(f"{indent}FAILED — {summary}", file=sys.stderr, flush=True)
339
+
340
+ with conn:
341
+ _write_meta(conn)
342
+ _finalize(conn, t0, skipped)
343
+ if failed:
344
+ print(f"\n{len(failed)} repo(s) failed and were skipped:",
345
+ file=sys.stderr)
346
+ for repo, msg in failed:
347
+ print(f" {repo}: {msg}", file=sys.stderr)
348
+ print(
349
+ "Re-run with -R to retry just those repos, e.g.:\n"
350
+ " jartify-locate --init --status=step "
351
+ + " ".join(f"-R {r}" for r, _ in failed[:3])
352
+ + ("" if len(failed) <= 3 else " …"),
353
+ file=sys.stderr,
354
+ )
355
+ finally:
356
+ conn.close()
357
+ return total_appended
358
+
359
+
290
360
  def _init_index(
291
361
  client: ArtifactoryClient,
292
362
  repos: list[str] | None,
@@ -296,6 +366,8 @@ def _init_index(
296
366
  print(f"Fetching artifact index for {scope} via AQL…", flush=True)
297
367
  if status == "progress":
298
368
  return _init_paginated(client, repos)
369
+ if status == "step":
370
+ return _init_step(client, repos)
299
371
  return _init_single_shot(client, repos)
300
372
 
301
373
 
@@ -314,11 +386,13 @@ def _glob_to_like(pattern: str) -> str:
314
386
 
315
387
 
316
388
  _JOIN_SELECT = (
317
- "SELECT l.repo, l.path, a.sha256, a.filename "
389
+ "SELECT l.repo, l.path, a.sha256, a.filename, a.size, a.created, a.modified "
318
390
  "FROM locations l "
319
391
  "JOIN artifacts a ON a.sha256 = l.sha256 AND a.filename = l.filename"
320
392
  )
321
393
 
394
+ _ROW_KEYS = ("repo", "path", "sha256", "filename", "size", "created", "modified")
395
+
322
396
 
323
397
  def _search_glob(conn, pattern: str | None, repos: list[str] | None, limit: int):
324
398
  """Pattern of None lists every (file, location) row."""
@@ -350,7 +424,7 @@ def _search_fuzzy(conn, query: str, repos: list[str] | None, limit: int):
350
424
  out = []
351
425
  for row in conn.execute(sql, params):
352
426
  if rx.search(row[3]):
353
- out.append(row[:3])
427
+ out.append(row)
354
428
  if len(out) >= limit:
355
429
  break
356
430
  return out
@@ -374,6 +448,26 @@ def _print_rows(rows) -> None:
374
448
  print(fmt.format(*row).rstrip())
375
449
 
376
450
 
451
+ def _print_json(rows) -> None:
452
+ print(json.dumps([dict(zip(_ROW_KEYS, r)) for r in rows], indent=2))
453
+
454
+
455
+ _EPILOG = """\
456
+ examples:
457
+ jartify-locate --init build the index (fast, single commit)
458
+ jartify-locate --init --status=progress paginated AQL with a live progress bar
459
+ jartify-locate --init --status=step one AQL per repo, prints each repo name
460
+ jartify-locate --init --all scan every visible repo, ignore filters
461
+ jartify-locate --init -R pypi-local refresh just one repo
462
+ jartify-locate list every indexed file
463
+ jartify-locate '*.json' glob
464
+ jartify-locate requests substring (no wildcards)
465
+ jartify-locate --fuzzy foojsn subsequence match
466
+ jartify-locate --all --json dump every indexed file as JSON
467
+ jartify-locate --db show index path + row counts
468
+ """
469
+
470
+
377
471
  def _build_parser() -> argparse.ArgumentParser:
378
472
  p = argparse.ArgumentParser(
379
473
  prog="jartify-locate",
@@ -381,6 +475,8 @@ def _build_parser() -> argparse.ArgumentParser:
381
475
  "locate(1)-style search over an offline SQLite index of your "
382
476
  "Artifactory. Run with --init first to build the index."
383
477
  ),
478
+ epilog=_EPILOG,
479
+ formatter_class=argparse.RawDescriptionHelpFormatter,
384
480
  )
385
481
  p.add_argument(
386
482
  "pattern",
@@ -399,16 +495,20 @@ def _build_parser() -> argparse.ArgumentParser:
399
495
  "ARTIFACTORY_REPOSITORIES, and the yaml config.")
400
496
  p.add_argument("--limit", type=int, default=500,
401
497
  help="Max rows to print or fuzzy-scan (default 500).")
498
+ p.add_argument("--json", action="store_true",
499
+ help="Emit JSON (every column: repo, path, sha256, filename, "
500
+ "size, created, modified) instead of the aligned table.")
402
501
  p.add_argument("--db", action="store_true",
403
502
  help="Print the index path and a one-line summary, then exit.")
404
503
  p.add_argument("--insecure", action="store_true",
405
504
  help="Skip TLS cert verification during --init.")
406
505
  p.add_argument(
407
506
  "--status",
408
- choices=("off", "progress"),
507
+ choices=("off", "progress", "step"),
409
508
  default="off",
410
- help="Progress display during --init. 'progress' paginates the AQL "
411
- "query and renders a live bar + count (default: off).",
509
+ help="Progress display during --init. 'progress' paginates AQL and "
510
+ "renders a live bar; 'step' runs one AQL per repo and prints "
511
+ "each repo name as it goes (default: off).",
412
512
  )
413
513
  p.add_argument("--version", action="version", version=f"jartify {__version__}")
414
514
  return p
@@ -500,7 +600,10 @@ def main(argv: list[str] | None = None) -> int:
500
600
  finally:
501
601
  conn.close()
502
602
 
503
- _print_rows(rows)
603
+ if args.json:
604
+ _print_json(rows)
605
+ else:
606
+ _print_rows(rows)
504
607
  return 0 if rows else 1
505
608
 
506
609
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jartify
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: TUI and CLI for searching JFrog Artifactory artifacts (version, filename, sha256)
5
5
  Author-email: Jakob Holst <code@jakobholst.dk>
6
6
  License: MIT
@@ -134,6 +134,9 @@ jartify-locate --init
134
134
  # commits so Ctrl+C keeps everything already indexed):
135
135
  jartify-locate --init --status=progress
136
136
 
137
+ # Or one AQL per repo, printing the repo name as it goes (commits per repo):
138
+ jartify-locate --init --status=step
139
+
137
140
  # Then search locally:
138
141
  jartify-locate # list everything (respects --repo / --limit)
139
142
  jartify-locate '*.json' # glob
@@ -1 +0,0 @@
1
- __version__ = "0.2.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes