utdquake 0.2.2__tar.gz → 0.2.3__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 (38) hide show
  1. {utdquake-0.2.2/utdquake.egg-info → utdquake-0.2.3}/PKG-INFO +1 -1
  2. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/__init__.py +12 -2
  3. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/bank/bank.py +6 -5
  4. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/bank/utils.py +21 -12
  5. utdquake-0.2.3/utdquake/figures.py +285 -0
  6. utdquake-0.2.3/utdquake/hub.py +351 -0
  7. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/catalog.py +67 -34
  8. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/travel_time.py +84 -2
  9. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/cache.py +1 -1
  10. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/plot.py +4 -4
  11. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/utils.py +26 -0
  12. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/parquet.py +90 -1
  13. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/schema.py +6 -3
  14. {utdquake-0.2.2 → utdquake-0.2.3/utdquake.egg-info}/PKG-INFO +1 -1
  15. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/SOURCES.txt +2 -0
  16. {utdquake-0.2.2 → utdquake-0.2.3}/LICENSE +0 -0
  17. {utdquake-0.2.2 → utdquake-0.2.3}/README.md +0 -0
  18. {utdquake-0.2.2 → utdquake-0.2.3}/setup.cfg +0 -0
  19. {utdquake-0.2.2 → utdquake-0.2.3}/setup.py +0 -0
  20. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/bank/__init__.py +0 -0
  21. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/__init__.py +0 -0
  22. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/config.py +0 -0
  23. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/data.py +0 -0
  24. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/load.py +0 -0
  25. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/patch.py +0 -0
  26. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/utdquake.py +0 -0
  27. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/utils.py +0 -0
  28. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/__init__.py +0 -0
  29. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/config.py +0 -0
  30. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/events.py +0 -0
  31. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/log.py +0 -0
  32. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/picks.py +0 -0
  33. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/__init__.py +0 -0
  34. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/__init__.py +0 -0
  35. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/manifest.py +0 -0
  36. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/dependency_links.txt +0 -0
  37. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/requires.txt +0 -0
  38. {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: utdquake
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: University of Texas at Dallas Earthquake Dataset
5
5
  Home-page: https://github.com/ecastillot/UTDQuake
6
6
  Author: ecastillot (Emmanuel Castillo)
@@ -9,6 +9,10 @@ includes:
9
9
  - `Network`: Class for network-specific access, including EventBank, picks, and stations.
10
10
  - `download_snapshot`: Function to download UTDQuake data from Hugging Face.
11
11
  - `load`: Function to load datasets from Hugging Face by key.
12
+ - `publish_network`: Function to publish a network's manifests to the shared Hub repo.
13
+ - `remove_network`: Function to remove a network's files from the shared Hub repo.
14
+ - `generate_network_figures`: Function to generate a network's documentation figures.
15
+ - `publish_network_figures`: Function to publish those figures to GitHub.
12
16
 
13
17
  Usage
14
18
  -----
@@ -20,11 +24,17 @@ Usage
20
24
  >>> net.events.head()
21
25
 
22
26
  """
23
- __version__ = "0.2.2"
27
+ __version__ = "0.2.3"
24
28
 
25
29
  from .core.utdquake import Dataset, Network
26
30
  from .core.data import download_snapshot, load
27
31
  from .core.patch import utdquake_obspy_patch
32
+ from .hub import publish_network, remove_network
33
+ from .figures import generate_network_figures, publish_network_figures
28
34
 
29
35
  utdquake_obspy_patch()
30
- __all__ = ["Dataset", "Network", "download_snapshot", "load"]
36
+ __all__ = [
37
+ "Dataset", "Network", "download_snapshot", "load",
38
+ "publish_network", "remove_network",
39
+ "generate_network_figures", "publish_network_figures",
40
+ ]
@@ -73,6 +73,7 @@ class UTDQBank(obsplus.EventBank):
73
73
  self.das = das
74
74
  self.contributor = os.path.basename(self.bank_path)
75
75
  self.db_paths = self.__prepare_paths()
76
+ self.utdq_paths = self.db_paths
76
77
 
77
78
  def __prepare_paths(self) -> None:
78
79
  """
@@ -297,7 +298,7 @@ class UTDQBank(obsplus.EventBank):
297
298
  raise FileNotFoundError(f"Folder not found: {folder_path}")
298
299
 
299
300
  # Build full glob pattern (e.g., /path/to/folder/*.xml)
300
- pattern = os.path.join(folder_path, file_extension)
301
+ pattern = os.path.join(folder_path, "**", file_extension)
301
302
 
302
303
  # Retrieve all matching files
303
304
  files: List[str] = glob.glob(pattern, recursive=True)
@@ -373,9 +374,9 @@ class UTDQBank(obsplus.EventBank):
373
374
  ebank_index_path = os.path.join(self.bank_path, ".index.db")
374
375
 
375
376
  if starttime is None:
376
- starttime = self.read_index()["time"].min()
377
+ starttime = self.read_index()["time"].min() - pd.Timedelta(seconds=1)
377
378
  if endtime is None:
378
- endtime = self.read_index()["time"].max()
379
+ endtime = self.read_index()["time"].max() + pd.Timedelta(seconds=1)
379
380
 
380
381
  iteration = 0
381
382
  for catalog_dict in ut.catalog_generator(self,starttime=starttime,
@@ -536,7 +537,7 @@ class UTDQBank(obsplus.EventBank):
536
537
  i,
537
538
  total_chunks,
538
539
  )
539
- catalog.apply_utdq_qc()
540
+ catalog = catalog.apply_utdq_qc()
540
541
 
541
542
  events_df = catalog.utdq_events_to_df()
542
543
  picks_df = catalog.utdq_picks_to_df()
@@ -628,7 +629,7 @@ class UTDQBank(obsplus.EventBank):
628
629
  )
629
630
  """
630
631
 
631
- df = ut._read_table(self.index_path, query)
632
+ df = ut._read_table(self.utdq_paths[".utdquake/export/db/stations"], query)
632
633
 
633
634
  df = sanitize_dataframe(df,
634
635
  order_cols=PREF_STATIONS_ORDER,
@@ -1228,11 +1228,14 @@ def get_stations_summary(stations_folder,
1228
1228
  # --- Collapse into one row ---
1229
1229
  summary = {}
1230
1230
  exclude_cols = {"dist_deg", "esaz", "used", "origin_id", "creation_time"}
1231
+ std_cols = {"calculated_latitude", "calculated_longitude"}
1231
1232
  for col in df.columns:
1232
1233
  if col in exclude_cols:
1233
1234
  continue
1234
1235
  if pd.api.types.is_float_dtype(df[col]):
1235
1236
  summary[col] = df[col].mean()
1237
+ if col in std_cols:
1238
+ summary[f"{col}_std"] = df[col].std()
1236
1239
  elif pd.api.types.is_bool_dtype(df[col]):
1237
1240
  summary[col] = df[col].any() # True if any True
1238
1241
  elif pd.api.types.is_integer_dtype(df[col]):
@@ -1242,6 +1245,12 @@ def get_stations_summary(stations_folder,
1242
1245
  else:
1243
1246
  summary[col] = df[col].iloc[0] # fallback
1244
1247
 
1248
+ # Number of arrivals that contributed to the calculated position
1249
+ # (one row per origin at this point, since drop_duplicates above
1250
+ # collapses to one entry per origin_id).
1251
+ if "calculated_latitude" in df.columns:
1252
+ summary["calculated_num_entries"] = int(df["calculated_latitude"].notna().sum())
1253
+
1245
1254
  summary["db_path"] = db_path
1246
1255
  summary["creation_time"] = UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
1247
1256
  return summary
@@ -1294,15 +1303,15 @@ def process_arrival(arrival, source_loc,
1294
1303
  "network_type": None,
1295
1304
  "station": None,
1296
1305
  "channel": None,
1297
- "dist_deg": np.NaN,
1298
- "esaz": np.NaN,
1306
+ "dist_deg": np.nan,
1307
+ "esaz": np.nan,
1299
1308
  "confirmed": False,
1300
- "confirmed_latitude": np.NaN,
1301
- "confirmed_longitude": np.NaN,
1302
- "confirmed_elevation": np.NaN,
1309
+ "confirmed_latitude": np.nan,
1310
+ "confirmed_longitude": np.nan,
1311
+ "confirmed_elevation": np.nan,
1303
1312
  "calculated": False,
1304
- "calculated_latitude": np.NaN,
1305
- "calculated_longitude": np.NaN,
1313
+ "calculated_latitude": np.nan,
1314
+ "calculated_longitude": np.nan,
1306
1315
  "used": False,
1307
1316
  "origin_id": origin_id,
1308
1317
  "creation_time": UTCDateTime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
@@ -1395,7 +1404,7 @@ def process_arrival(arrival, source_loc,
1395
1404
  #finding the corresponding stations folder for the network
1396
1405
  network_name = os.path.basename(os.path.dirname(ebank_index_path))
1397
1406
  utdq_paths = get_utdq_paths(network_name)
1398
- stations_folder = utdq_paths[".utdquake/export/db/stations/.stations"]
1407
+ stations_folder = utdq_paths[".utdquake/export/db"] / "stations" / f".{network_name}"
1399
1408
 
1400
1409
  # stations_folder = os.path.join( stations_folder,".stations")
1401
1410
 
@@ -1586,8 +1595,8 @@ def xxx(origin, df_stations,
1586
1595
  missing_info ={
1587
1596
  "network": net,
1588
1597
  "station": sta,
1589
- "latitude": np.NaN,
1590
- "longitude": np.NaN,
1598
+ "latitude": np.nan,
1599
+ "longitude": np.nan,
1591
1600
  "status": "missing",
1592
1601
  "creation_time": UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S.%f")}
1593
1602
  missing_info = pd.DataFrame([missing_info])
@@ -1673,8 +1682,8 @@ def xxx(origin, df_stations,
1673
1682
  missing_info = {
1674
1683
  "network": net,
1675
1684
  "station": sta,
1676
- "latitude": np.NaN,
1677
- "longitude": np.NaN,
1685
+ "latitude": np.nan,
1686
+ "longitude": np.nan,
1678
1687
  "status": "missing",
1679
1688
  "creation_time": UTCDateTime.now().strftime("%Y-%m-%d %H:%M:%S.%f")}
1680
1689
  missing_info = pd.DataFrame([missing_info])
@@ -0,0 +1,285 @@
1
+ """
2
+ Generate and publish the standard per-network figure set used in the
3
+ UTDQuake documentation (:ref:`overview-section`).
4
+
5
+ Two independent halves:
6
+
7
+ - ``generate_network_figures``: purely local, no credentials. Runs
8
+ :class:`utdquake.Network`'s own ``plot_*`` methods and saves the results
9
+ with the ``{network}_{suffix}.png`` naming already used on the ``figures``
10
+ branch of https://github.com/ecastillot/UTDQuake.
11
+ - ``publish_network_figures``: uploads to GitHub. Needs a token
12
+ (``GITHUB_TOKEN`` env var or ``token=``) with ``repo`` scope.
13
+ - ``overview_rst_row``: read-only, no token needed -- the repo is public.
14
+
15
+ Unlike the Hugging Face Hub, GitHub does not let a non-collaborator open a
16
+ PR without forking first. If the token does not have write access to
17
+ ecastillot/UTDQuake, ``publish_network_figures`` forks the repo under the
18
+ token's account, pushes the figures to that fork's ``figures`` branch, and
19
+ opens a PR back to ecastillot/UTDQuake. If the token does have write access
20
+ (a maintainer's), it commits directly instead.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import base64
26
+ import logging
27
+ import time
28
+ from pathlib import Path
29
+ from typing import Dict, Optional
30
+
31
+ import requests
32
+
33
+ from .core.utdquake import Network
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ GITHUB_OWNER = "ecastillot"
38
+ GITHUB_REPO = "UTDQuake"
39
+ GITHUB_BRANCH = "figures"
40
+ GITHUB_TOKEN_ENV_VAR = "GITHUB_TOKEN"
41
+ GITHUB_API = "https://api.github.com"
42
+
43
+ # (suffix, Network method name, kwargs for that method, accepts a show= kwarg)
44
+ FIGURE_SPECS = [
45
+ ("overview", "plot_overview", {}, True),
46
+ ("stats", "plot_stats", {}, True),
47
+ ("pick_histograms", "plot_pick_histograms", {}, True),
48
+ ("phase_count_radar", "plot_phase_count_radar_by_magnitude", {}, True),
49
+ ("station_location_uncertainty", "plot_station_location_uncertainty", {}, True),
50
+ ("uncertainty_boxplots", "plot_uncertainty_boxplots", {}, True),
51
+ ("travel_time_qc", "plot_travel_time_qc", {}, False),
52
+ ("travel_time_vs_distance", "plot_travel_time_vs_distance", {}, True),
53
+ ("travel_time_vs_distance_P_zscore", "plot_travel_time_vs_distance_zscore", {"phase": "P"}, True),
54
+ ]
55
+
56
+
57
+ def generate_network_figures(
58
+ network: str,
59
+ outdir: Optional[Path] = None,
60
+ das: bool = False,
61
+ ) -> Dict[str, Path]:
62
+ """
63
+ Generate the standard figure set for a network.
64
+
65
+ Each figure is generated independently -- one missing a prerequisite
66
+ (e.g. no travel-time model yet) is logged and skipped rather than
67
+ failing the whole batch.
68
+
69
+ Parameters
70
+ ----------
71
+ network : str
72
+ Network code, e.g. "CM".
73
+ outdir : Path, optional
74
+ Directory to save into. Defaults to ``./figures/{network}``.
75
+ das : bool, optional
76
+ Whether this is a DAS network. Default False.
77
+
78
+ Returns
79
+ -------
80
+ dict[str, Path]
81
+ ``{suffix: saved_path}`` for every figure that succeeded.
82
+ """
83
+ net = Network(network, das=das)
84
+ outdir = Path(outdir) if outdir is not None else Path.cwd() / "figures" / network
85
+ outdir.mkdir(parents=True, exist_ok=True)
86
+
87
+ saved: Dict[str, Path] = {}
88
+ for suffix, method_name, kwargs, accepts_show in FIGURE_SPECS:
89
+ savepath = outdir / f"{network}_{suffix}.png"
90
+ call_kwargs = dict(savepath=str(savepath), **kwargs)
91
+ if accepts_show:
92
+ call_kwargs["show"] = False
93
+ try:
94
+ getattr(net, method_name)(**call_kwargs)
95
+ saved[suffix] = savepath
96
+ logger.info("Generated %s", savepath)
97
+ except Exception as e:
98
+ logger.warning("Skipping %s for network %s: %s", suffix, network, e)
99
+ return saved
100
+
101
+
102
+ def _resolve_token(token: Optional[str] = None) -> str:
103
+ import os
104
+ token = token or os.environ.get(GITHUB_TOKEN_ENV_VAR)
105
+ if not token:
106
+ raise ValueError(
107
+ f"No GitHub token provided. Set the {GITHUB_TOKEN_ENV_VAR} "
108
+ "environment variable or pass token=... explicitly."
109
+ )
110
+ return token
111
+
112
+
113
+ def _gh(method: str, url: str, token: str, **kwargs) -> requests.Response:
114
+ headers = {
115
+ "Authorization": f"Bearer {token}",
116
+ "Accept": "application/vnd.github+json",
117
+ "X-GitHub-Api-Version": "2022-11-28",
118
+ }
119
+ resp = requests.request(method, url, headers=headers, timeout=30, **kwargs)
120
+ if resp.status_code >= 400:
121
+ raise RuntimeError(f"GitHub API {method} {url} -> {resp.status_code}: {resp.text[:500]}")
122
+ return resp
123
+
124
+
125
+ def _authenticated_user(token: str) -> str:
126
+ return _gh("GET", f"{GITHUB_API}/user", token).json()["login"]
127
+
128
+
129
+ def _has_write_access(token: str, username: str) -> bool:
130
+ resp = requests.get(
131
+ f"{GITHUB_API}/repos/{GITHUB_OWNER}/{GITHUB_REPO}/collaborators/{username}/permission",
132
+ headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"},
133
+ timeout=30,
134
+ )
135
+ if resp.status_code != 200:
136
+ return False
137
+ return resp.json().get("permission") in ("write", "admin")
138
+
139
+
140
+ def _ensure_fork(token: str, username: str) -> None:
141
+ resp = requests.get(f"{GITHUB_API}/repos/{username}/{GITHUB_REPO}", headers={
142
+ "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"
143
+ }, timeout=30)
144
+ if resp.status_code == 200:
145
+ return
146
+ _gh("POST", f"{GITHUB_API}/repos/{GITHUB_OWNER}/{GITHUB_REPO}/forks", token)
147
+ # Forking is async -- poll until the fork is queryable.
148
+ for _ in range(20):
149
+ time.sleep(1.5)
150
+ resp = requests.get(f"{GITHUB_API}/repos/{username}/{GITHUB_REPO}", headers={
151
+ "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"
152
+ }, timeout=30)
153
+ if resp.status_code == 200:
154
+ return
155
+ raise RuntimeError(f"Timed out waiting for fork {username}/{GITHUB_REPO} to become available.")
156
+
157
+
158
+ def _put_file(owner: str, token: str, path_in_repo: str, local_path: Path, branch: str, message: str) -> None:
159
+ url = f"{GITHUB_API}/repos/{owner}/{GITHUB_REPO}/contents/{path_in_repo}"
160
+ existing = requests.get(url, params={"ref": branch}, headers={
161
+ "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"
162
+ }, timeout=30)
163
+ sha = existing.json().get("sha") if existing.status_code == 200 else None
164
+
165
+ content_b64 = base64.b64encode(local_path.read_bytes()).decode("ascii")
166
+ body = {"message": message, "content": content_b64, "branch": branch}
167
+ if sha:
168
+ body["sha"] = sha
169
+ _gh("PUT", url, token, json=body)
170
+
171
+
172
+ def publish_network_figures(
173
+ network: str,
174
+ local_dir: Optional[Path] = None,
175
+ token: Optional[str] = None,
176
+ create_pr: bool = True,
177
+ commit_message: Optional[str] = None,
178
+ ) -> str:
179
+ """
180
+ Publish a network's already-generated figures (see
181
+ :func:`generate_network_figures`) to the ``figures`` branch of
182
+ ecastillot/UTDQuake on GitHub.
183
+
184
+ Parameters
185
+ ----------
186
+ network : str
187
+ Network code, e.g. "CM".
188
+ local_dir : Path, optional
189
+ Directory holding the generated PNGs. Defaults to
190
+ ``./figures/{network}`` (matching ``generate_network_figures``'s
191
+ default).
192
+ token : str, optional
193
+ GitHub personal access token (``repo`` scope). Defaults to the
194
+ ``GITHUB_TOKEN`` environment variable.
195
+ create_pr : bool, optional
196
+ If True (default) and the token does *not* have write access to
197
+ ecastillot/UTDQuake, fork the repo under the token's account, push
198
+ the figures there, and open a PR back to ecastillot/UTDQuake -- no
199
+ write access to the upstream repo required. If the token *does*
200
+ have write access, this is ignored and the figures are committed
201
+ directly to ecastillot/UTDQuake's ``figures`` branch regardless of
202
+ this flag -- there's no reason to route a maintainer's own push
203
+ through a fork.
204
+ commit_message : str, optional
205
+ Commit message. Defaults to a generated message.
206
+
207
+ Returns
208
+ -------
209
+ str
210
+ URL of the resulting commit (or PR, if ``create_pr=True``).
211
+ """
212
+ token = _resolve_token(token)
213
+ local_dir = Path(local_dir) if local_dir is not None else Path.cwd() / "figures" / network
214
+ pngs = sorted(local_dir.glob(f"{network}_*.png"))
215
+ if not pngs:
216
+ raise FileNotFoundError(f"No {network}_*.png files in {local_dir}. Run generate_network_figures first.")
217
+
218
+ username = _authenticated_user(token)
219
+ message = commit_message or f"Add figures for network {network}"
220
+
221
+ if create_pr and not _has_write_access(token, username):
222
+ _ensure_fork(token, username)
223
+ target_owner = username
224
+ else:
225
+ target_owner = GITHUB_OWNER
226
+ create_pr = False # already a maintainer -- commit directly
227
+
228
+ for png in pngs:
229
+ _put_file(
230
+ owner=target_owner,
231
+ token=token,
232
+ path_in_repo=f"figures/networks/{network}/{png.name}",
233
+ local_path=png,
234
+ branch=GITHUB_BRANCH,
235
+ message=message,
236
+ )
237
+ logger.info("Uploaded %s to %s/%s@%s", png.name, target_owner, GITHUB_REPO, GITHUB_BRANCH)
238
+
239
+ if not create_pr:
240
+ return f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/tree/{GITHUB_BRANCH}/figures/networks/{network}"
241
+
242
+ pr = _gh(
243
+ "POST",
244
+ f"{GITHUB_API}/repos/{GITHUB_OWNER}/{GITHUB_REPO}/pulls",
245
+ token,
246
+ json={
247
+ "title": message,
248
+ "head": f"{username}:{GITHUB_BRANCH}",
249
+ "base": GITHUB_BRANCH,
250
+ "body": f"Adds the figure set for network `{network}` (see :ref:`upload-dataset-section`).",
251
+ },
252
+ ).json()
253
+ logger.info("Opened PR: %s", pr["html_url"])
254
+ return pr["html_url"]
255
+
256
+
257
+ def overview_rst_row(network: str, das: bool = False) -> str:
258
+ """
259
+ Return the ``overview.rst`` list-table row for a network, ready to
260
+ paste in: the real image row if figures exist on the ``figures``
261
+ branch, or a "pending" placeholder row if not.
262
+
263
+ This makes a single unauthenticated, read-only request -- no token
264
+ needed, since the repo is public.
265
+ """
266
+ raw_url = (
267
+ f"https://raw.githubusercontent.com/{GITHUB_OWNER}/{GITHUB_REPO}/"
268
+ f"{GITHUB_BRANCH}/figures/networks/{network}/{network}_overview.png"
269
+ )
270
+ folder_url = f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/tree/{GITHUB_BRANCH}/figures/networks/{network}"
271
+
272
+ exists = requests.head(raw_url, timeout=15).status_code == 200
273
+
274
+ if exists:
275
+ return (
276
+ f" * - {network}\n"
277
+ f" - `Open Folder <{folder_url}>`_\n"
278
+ f" - .. image:: {raw_url}\n"
279
+ f" :width: 200px\n"
280
+ )
281
+ return (
282
+ f" * - {network}\n"
283
+ f" - *pending*\n"
284
+ f" - *Figures pending -- see* :ref:`upload-dataset-section`\n"
285
+ )