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.
- {utdquake-0.2.2/utdquake.egg-info → utdquake-0.2.3}/PKG-INFO +1 -1
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/__init__.py +12 -2
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/bank/bank.py +6 -5
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/bank/utils.py +21 -12
- utdquake-0.2.3/utdquake/figures.py +285 -0
- utdquake-0.2.3/utdquake/hub.py +351 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/catalog.py +67 -34
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/travel_time.py +84 -2
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/cache.py +1 -1
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/plot.py +4 -4
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/utils.py +26 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/parquet.py +90 -1
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/schema.py +6 -3
- {utdquake-0.2.2 → utdquake-0.2.3/utdquake.egg-info}/PKG-INFO +1 -1
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/SOURCES.txt +2 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/LICENSE +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/README.md +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/setup.cfg +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/setup.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/bank/__init__.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/__init__.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/config.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/data.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/load.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/patch.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/utdquake.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/core/utils.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/__init__.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/config.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/events.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/log.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/qc/picks.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/utils/__init__.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/__init__.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake/writers/manifest.py +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/dependency_links.txt +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/requires.txt +0 -0
- {utdquake-0.2.2 → utdquake-0.2.3}/utdquake.egg-info/top_level.txt +0 -0
|
@@ -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.
|
|
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__ = [
|
|
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.
|
|
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.
|
|
1298
|
-
"esaz": np.
|
|
1306
|
+
"dist_deg": np.nan,
|
|
1307
|
+
"esaz": np.nan,
|
|
1299
1308
|
"confirmed": False,
|
|
1300
|
-
"confirmed_latitude": np.
|
|
1301
|
-
"confirmed_longitude": np.
|
|
1302
|
-
"confirmed_elevation": np.
|
|
1309
|
+
"confirmed_latitude": np.nan,
|
|
1310
|
+
"confirmed_longitude": np.nan,
|
|
1311
|
+
"confirmed_elevation": np.nan,
|
|
1303
1312
|
"calculated": False,
|
|
1304
|
-
"calculated_latitude": np.
|
|
1305
|
-
"calculated_longitude": np.
|
|
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
|
|
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.
|
|
1590
|
-
"longitude": np.
|
|
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.
|
|
1677
|
-
"longitude": np.
|
|
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
|
+
)
|