pyrox-client 0.0.1__tar.gz → 0.0.2__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,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrox-client
3
- Version: 0.0.1
4
- Summary: HYROX race data client - retrieve full race results via Python
5
- Project-URL: homepage, https://github.com/<you>/pyrox
3
+ Version: 0.0.2
4
+ Summary: HYROX race data client retrieve full race results via Python
6
5
  Author: Vlad Matei
7
6
  License: MIT
8
7
  Requires-Python: >=3.9
9
- Requires-Dist: fastparquet>=15
10
- Requires-Dist: pandas>=2.0
8
+ Requires-Dist: fastparquet>=2024.5.0
9
+ Requires-Dist: fsspec>=2024.2.0
10
+ Requires-Dist: pandas>=2.2
11
+ Requires-Dist: platformdirs>=4.2.0
11
12
  Requires-Dist: requests>=2.31
13
+ Requires-Dist: s3fs>=2024.2.0
12
14
  Provides-Extra: duckdb
13
15
  Requires-Dist: duckdb>=1.0.0; extra == 'duckdb'
14
16
  Description-Content-Type: text/markdown
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.20"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyrox-client"
7
+ version = "0.0.2"
8
+ description = "HYROX race data client – retrieve full race results via Python"
9
+ authors = [{ name = "Vlad Matei" }]
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.9"
13
+ dependencies = [
14
+ "pandas>=2.2",
15
+ "fastparquet>=2024.5.0",
16
+ "fsspec>=2024.2.0",
17
+ "s3fs>=2024.2.0",
18
+ "requests>=2.31",
19
+ "platformdirs>=4.2.0",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ duckdb = ["duckdb>=1.0.0"]
24
+
25
+
26
+ # You’re using a src/ layout. This tells Hatch which package to build.
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/pyrox"]
29
+
30
+ # Ensure sdist includes your source + docs
31
+ [tool.hatch.build.targets.sdist]
32
+ include = [
33
+ "src/pyrox",
34
+ "README.md",
35
+ "pyproject.toml"
36
+ ]
@@ -0,0 +1,5 @@
1
+ from .core import get_race, list_races
2
+ from .config import set_bucket, set_manifest_path, get_config
3
+
4
+ __all__ = ["get_race", "list_races", "set_bucket", "set_manifest_path", "get_config"]
5
+ __version__ = "0.0.2"
@@ -0,0 +1,31 @@
1
+ from dataclasses import dataclass
2
+ from platformdirs import user_cache_dir
3
+ from pathlib import Path
4
+ import os
5
+
6
+ APP_NAME = "pyrox"
7
+ APP_AUTHOR = "pyrox"
8
+
9
+ _DEFAULT_BUCKET = os.getenv("PYROX_BUCKET", "s3://hyrox-results")
10
+ _DEFAULT_MANIFEST = os.getenv("PYROX_MANIFEST_PATH", "processed/manifest/latest.csv")
11
+
12
+ @dataclass
13
+ class PyroxConfig:
14
+ bucket: str = _DEFAULT_BUCKET
15
+ manifest_path: str = _DEFAULT_MANIFEST
16
+ cache_dir:Path = Path(user_cache_dir(APP_NAME, APP_AUTHOR))
17
+
18
+ _config = PyroxConfig()
19
+
20
+
21
+ def set_bucket(uri:str) -> None:
22
+ _config.bucket = uri.rsplit("/")
23
+
24
+
25
+ def set_manifest_path(path: str) -> None:
26
+ _config.manifest_path = path
27
+
28
+
29
+ def get_config() -> PyroxConfig:
30
+ _config.cache_dir.mkdir(parents=True, exist_ok=True)
31
+ return _config
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ from pathlib import Path
5
+ import pandas as pd
6
+
7
+ from .manifest import load_manifest, _s3_open # re-use your manifest loader + fsspec opener
8
+ from .config import get_config
9
+ from .errors import RaceNotFound, ParquetReadError
10
+
11
+
12
+ def list_races(season: int | None = None) -> pd.DataFrame:
13
+ """
14
+ Return available (season, location) pairs from the manifest.
15
+ """
16
+ df = load_manifest(refresh=True)
17
+ if season is not None:
18
+ try:
19
+ df = df[df["season"] == int(season)]
20
+ except ValueError as e:
21
+ raise (ValueError(f"Input {season} is not of expected type. Please make sure you request an integer."))
22
+ return (
23
+ df[["season", "location"]]
24
+ .drop_duplicates()
25
+ .sort_values(["season", "location"])
26
+ .reset_index(drop=True)
27
+ )
28
+
29
+
30
+ def _local_parquet_cache_path(s3_key: str) -> Path:
31
+ """
32
+ Map an S3 key to a deterministic local cache path under ~/.cache/pyrox/parquet/.
33
+ """
34
+ cfg = get_config()
35
+ p = cfg.cache_dir / "parquet" / s3_key.replace("/", "__")
36
+ p.parent.mkdir(parents=True, exist_ok=True)
37
+ return p
38
+
39
+
40
+ def _read_parquet_from_s3(s3_uri: str) -> pd.DataFrame:
41
+ """
42
+ Download the parquet bytes anonymously from S3 and read with fastparquet.
43
+ """
44
+ try:
45
+ with _s3_open(s3_uri, "rb", anon=True) as f:
46
+ data = f.read()
47
+ return pd.read_parquet(io.BytesIO(data), engine="fastparquet")
48
+ except Exception as e:
49
+ raise ParquetReadError(f"Failed to read parquet from {s3_uri}: {e}")
50
+ # core.py
51
+ def _to_s3_uri(bucket: str, key: str) -> str:
52
+ """Join bucket + key safely. If key is already a full s3:// URI, use it as-is."""
53
+ key = key.strip()
54
+ if key.startswith("s3://"):
55
+ return key
56
+ # allow bucket to be "s3://bucket" or just "bucket"
57
+ if not bucket.startswith("s3://"):
58
+ bucket = f"s3://{bucket}"
59
+ return f"{bucket.rstrip('/')}/{key.lstrip('/')}"
60
+
61
+
62
+ def get_race(*, season: int, location: str) -> pd.DataFrame:
63
+ """
64
+ Resolve (season, location) -> s3_key via the manifest, then return the race DataFrame.
65
+
66
+ Uses a simple local file cache. If the cached parquet fails to read,
67
+ it is discarded and we re-fetch from S3.
68
+ """
69
+ manifest = load_manifest(refresh=True)
70
+
71
+ # case-insensitive match on location
72
+ row = manifest[
73
+ (manifest["season"] == int(season)) &
74
+ (manifest["location"].str.casefold() == location.casefold())
75
+ ]
76
+
77
+ if row.empty:
78
+ raise RaceNotFound(f"No race found for season={season}, location='{location}'. "
79
+ f"Try: pyrox.list_races({season})")
80
+
81
+ s3_key = row.iloc[0]["path"]
82
+ cfg = get_config()
83
+ s3_uri = _to_s3_uri(cfg.bucket, s3_key)
84
+
85
+ # check local cache first
86
+ local_path = _local_parquet_cache_path(s3_key)
87
+ if local_path.exists():
88
+ try:
89
+ return pd.read_parquet(local_path, engine="fastparquet")
90
+ except Exception:
91
+ local_path.unlink(missing_ok=True) # bad cache → remove and refetch
92
+
93
+ # fetch from S3 and cache
94
+ df = _read_parquet_from_s3(s3_uri)
95
+ try:
96
+ df.to_parquet(local_path, index=False, engine="fastparquet")
97
+ except Exception:
98
+ # cache failures should not fail the API
99
+ pass
100
+ return df
101
+
102
+ breakheere = 0
@@ -0,0 +1,60 @@
1
+ """
2
+ Custom exceptions for the pyrox client.
3
+
4
+ Expose a small, predictable hierarchy so users can:
5
+ - catch a broad PyroxError for any client-side issue, or
6
+ - catch specific errors for targeted handling (e.g., missing race).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ __all__ = [
13
+ "PyroxError",
14
+ "ConfigError",
15
+ "ManifestUnavailable",
16
+ "RaceNotFound",
17
+ "ParquetReadError",
18
+ ]
19
+
20
+
21
+ class PyroxError(Exception):
22
+ """Base class for all pyrox-specific errors."""
23
+ pass
24
+
25
+
26
+ class ConfigError(PyroxError):
27
+ """
28
+ Raised when the client configuration is invalid or incomplete.
29
+ Example: bucket URI not set or malformed.
30
+ """
31
+ pass
32
+
33
+
34
+ class ManifestUnavailable(PyroxError):
35
+ """
36
+ Raised when the manifest cannot be retrieved or parsed.
37
+
38
+ Typical reasons:
39
+ - Network/S3 access problems
40
+ - File missing or not public
41
+ - CSV missing required columns (season, location, s3_key)
42
+ """
43
+ pass
44
+
45
+
46
+ class RaceNotFound(PyroxError, LookupError):
47
+ """
48
+ Raised when (season, location) is not present in the manifest.
49
+
50
+ Inherit from LookupError to feel natural in 'lookup' code paths.
51
+ """
52
+ pass
53
+
54
+
55
+ class ParquetReadError(PyroxError, IOError):
56
+ """
57
+ Raised when the parquet for a race cannot be read (S3 read failure,
58
+ corrupted object, incompatible parquet version, etc.).
59
+ """
60
+ pass
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ from typing import Optional, Tuple
5
+ from pathlib import Path
6
+
7
+ import fsspec
8
+ import pandas as pd
9
+
10
+ from .config import get_config
11
+ from .errors import ManifestUnavailable
12
+
13
+
14
+ def _s3_open(path: str, mode: str = "rb", anon: bool = True):
15
+ """
16
+ Unified opener for S3 (via fsspec/s3fs).
17
+
18
+ Parameters
19
+ ----------
20
+ path : str
21
+ Full fsspec path, e.g. "s3://my-bucket/processed/manifest/latest.csv"
22
+ mode : str
23
+ File mode, usually "rb" for binary reads.
24
+ anon : bool
25
+ True => anonymous/public read (no AWS creds required).
26
+
27
+ Returns
28
+ -------
29
+ contextmanager
30
+ Use with 'with _s3_open(...) as f: data = f.read()'
31
+ """
32
+ # s3fs accepts the 'anon' kw to do public/unsigned requests
33
+ return fsspec.open(path, mode=mode, **({"anon": True} if anon else {}))
34
+
35
+
36
+ def _cache_paths(name: str) -> Tuple[Path, Path]:
37
+ """
38
+ Compute cache locations for the manifest CSV and its ETag sidecar.
39
+
40
+ We cache:
41
+ - the CSV bytes as ~/.cache/pyrox/manifest.latest.csv
42
+ - the ETag string as ~/.cache/pyrox/manifest.latest.csv.etag
43
+ """
44
+ cfg = get_config()
45
+ cache_file = cfg.cache_dir / name
46
+ meta_file = cfg.cache_dir / f"{name}.etag"
47
+ return cache_file, meta_file
48
+
49
+
50
+ def _read_etag(meta_file: Path) -> Optional[str]:
51
+ """Read the cached ETag string (if any)."""
52
+ if meta_file.exists():
53
+ try:
54
+ etag = meta_file.read_text().strip()
55
+ return etag or None
56
+ except Exception:
57
+ return None
58
+ return None
59
+
60
+
61
+ def _head_s3(path: str) -> dict:
62
+ """
63
+ Fetch object metadata from S3 (cheap 'HEAD').
64
+
65
+ Returns a dict similar to:
66
+ {
67
+ "ETag": "\"5d41402abc...\"",
68
+ "LastModified": datetime,
69
+ "Size": int,
70
+ ...
71
+ }
72
+
73
+ Notes
74
+ -----
75
+ - We obtain the filesystem object from an open file handle (fo.fs),
76
+ then call fs.info(path).
77
+ - Not all gateways populate all keys—but ETag is common for S3.
78
+ """
79
+ # Open briefly to access underlying filesystem handle
80
+ with fsspec.open(path, mode="rb", anon=True) as fo:
81
+ fs = fo.fs # type: ignore[attr-defined]
82
+ return fs.info(path)
83
+
84
+
85
+ def load_manifest(refresh: bool = True) -> pd.DataFrame:
86
+ """
87
+ Load the manifest CSV as a pandas DataFrame with required columns:
88
+ ['season', 'location', 's3_key']
89
+
90
+ Caching strategy
91
+ ----------------
92
+ - Store the last-downloaded manifest under ~/.cache/pyrox.
93
+ - On each call (when refresh=True), perform a HEAD request to get the
94
+ current ETag. If it matches the cached ETag -> use cached CSV.
95
+ Otherwise, re-download and update the cache.
96
+
97
+ Parameters
98
+ ----------
99
+ refresh : bool
100
+ If False, skip HEAD check and use cached file if present.
101
+ If True (default), validate with ETag before deciding.
102
+
103
+ Raises
104
+ ------
105
+ ManifestUnavailable
106
+ If we cannot fetch or parse the manifest, and there is no usable cache.
107
+
108
+ Returns
109
+ -------
110
+ pd.DataFrame
111
+ Normalized manifest DataFrame.
112
+ """
113
+ cfg = get_config()
114
+ s3_manifest_uri = f"{cfg.bucket}/{cfg.manifest_path}"
115
+ cache_file, meta_file = _cache_paths("manifest.latest.csv")
116
+
117
+ # 1) Try to fetch latest ETag (safe to fail; we'll fall back).
118
+ current_etag: Optional[str] = None
119
+ if refresh:
120
+ try:
121
+ info = _head_s3(s3_manifest_uri)
122
+ # Some backends wrap ETag in quotes; we keep it as-is because
123
+ # we compare exact strings on both sides.
124
+ current_etag = info.get("ETag")
125
+ except Exception:
126
+ # HEAD can fail (e.g., transient network); we'll fall back to cache or GET.
127
+ current_etag = None
128
+
129
+ # 2) If we have a cached file and either:
130
+ # - HEAD failed (no current_etag), or
131
+ # - ETag matches the cached one,
132
+ # then load cached CSV immediately.
133
+ cached_etag = _read_etag(meta_file)
134
+ if cache_file.exists() and (current_etag is None or cached_etag == current_etag):
135
+ try:
136
+ return pd.read_csv(cache_file)
137
+ except Exception:
138
+ # Corrupted cache? Continue to re-download below.
139
+ pass
140
+
141
+ # 3) Re-download from S3 and update cache.
142
+ try:
143
+ with _s3_open(s3_manifest_uri, "rb", anon=True) as f:
144
+ data = f.read()
145
+
146
+ # Parse CSV from bytes buffer
147
+ df = pd.read_csv(io.BytesIO(data))
148
+
149
+ # Persist cache on success
150
+ cache_file.write_bytes(data)
151
+ if current_etag:
152
+ meta_file.write_text(current_etag)
153
+
154
+ # Basic schema checks
155
+ for col in ("season", "location", "s3_key"):
156
+ if col not in df.columns:
157
+ raise ManifestUnavailable(f"Manifest missing required column: {col}")
158
+
159
+ # Normalize dtypes; keeps downstream filters predictable
160
+ df["season"] = df["season"].astype(int)
161
+ df["location"] = df["location"].astype(str)
162
+ df["s3_key"] = df["s3_key"].astype(str)
163
+
164
+ return df
165
+
166
+ except Exception as e:
167
+ # 4) If network failed, try to limp with the existing cache (even if ETag mismatch).
168
+ if cache_file.exists():
169
+ try:
170
+ return pd.read_csv(cache_file)
171
+ except Exception:
172
+ pass
173
+ # No working cache → bail with a clear error
174
+ raise ManifestUnavailable(f"Could not load manifest from {s3_manifest_uri}: {e}")
@@ -1,3 +0,0 @@
1
- # Default ignored files
2
- /shelf/
3
- /workspace.xml
@@ -1 +0,0 @@
1
- pyrox-client
@@ -1,392 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
5
- <option name="ignoredPackages">
6
- <value>
7
- <list size="1">
8
- <item index="0" class="java.lang.String" itemvalue="lxml" />
9
- </list>
10
- </value>
11
- </option>
12
- </inspection_tool>
13
- <inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
14
- <option name="ignoredErrors">
15
- <list>
16
- <option value="W29" />
17
- <option value="E501" />
18
- <option value="W29" />
19
- <option value="E501" />
20
- <option value="W29" />
21
- <option value="E501" />
22
- <option value="W29" />
23
- <option value="E501" />
24
- <option value="W29" />
25
- <option value="E501" />
26
- <option value="W29" />
27
- <option value="E501" />
28
- <option value="W29" />
29
- <option value="E501" />
30
- <option value="W29" />
31
- <option value="E501" />
32
- <option value="W29" />
33
- <option value="E501" />
34
- <option value="W29" />
35
- <option value="E501" />
36
- <option value="W29" />
37
- <option value="E501" />
38
- <option value="W29" />
39
- <option value="E501" />
40
- <option value="W29" />
41
- <option value="E501" />
42
- <option value="W29" />
43
- <option value="E501" />
44
- <option value="W29" />
45
- <option value="E501" />
46
- <option value="W29" />
47
- <option value="E501" />
48
- <option value="W29" />
49
- <option value="E501" />
50
- <option value="W29" />
51
- <option value="E501" />
52
- <option value="W29" />
53
- <option value="E501" />
54
- <option value="W29" />
55
- <option value="E501" />
56
- <option value="W29" />
57
- <option value="E501" />
58
- <option value="W29" />
59
- <option value="E501" />
60
- <option value="W29" />
61
- <option value="E501" />
62
- <option value="W29" />
63
- <option value="E501" />
64
- <option value="W29" />
65
- <option value="E501" />
66
- <option value="W29" />
67
- <option value="E501" />
68
- <option value="W29" />
69
- <option value="E501" />
70
- <option value="W29" />
71
- <option value="E501" />
72
- <option value="W29" />
73
- <option value="E501" />
74
- <option value="W29" />
75
- <option value="E501" />
76
- <option value="W29" />
77
- <option value="E501" />
78
- <option value="W29" />
79
- <option value="E501" />
80
- <option value="W29" />
81
- <option value="E501" />
82
- <option value="W29" />
83
- <option value="E501" />
84
- <option value="W29" />
85
- <option value="E501" />
86
- <option value="W29" />
87
- <option value="E501" />
88
- <option value="W29" />
89
- <option value="E501" />
90
- <option value="W29" />
91
- <option value="E501" />
92
- <option value="W29" />
93
- <option value="E501" />
94
- <option value="W29" />
95
- <option value="E501" />
96
- <option value="W29" />
97
- <option value="E501" />
98
- <option value="W29" />
99
- <option value="E501" />
100
- <option value="W29" />
101
- <option value="E501" />
102
- <option value="W29" />
103
- <option value="E501" />
104
- <option value="W29" />
105
- <option value="E501" />
106
- <option value="W29" />
107
- <option value="E501" />
108
- <option value="W29" />
109
- <option value="E501" />
110
- <option value="W29" />
111
- <option value="E501" />
112
- <option value="W29" />
113
- <option value="E501" />
114
- <option value="W29" />
115
- <option value="E501" />
116
- <option value="W29" />
117
- <option value="E501" />
118
- <option value="W29" />
119
- <option value="E501" />
120
- <option value="W29" />
121
- <option value="E501" />
122
- <option value="W29" />
123
- <option value="E501" />
124
- <option value="W29" />
125
- <option value="E501" />
126
- <option value="W29" />
127
- <option value="E501" />
128
- <option value="W29" />
129
- <option value="E501" />
130
- <option value="W29" />
131
- <option value="E501" />
132
- <option value="W29" />
133
- <option value="E501" />
134
- <option value="W29" />
135
- <option value="E501" />
136
- <option value="W29" />
137
- <option value="E501" />
138
- <option value="W29" />
139
- <option value="E501" />
140
- <option value="W29" />
141
- <option value="E501" />
142
- <option value="W29" />
143
- <option value="E501" />
144
- <option value="W29" />
145
- <option value="E501" />
146
- <option value="W29" />
147
- <option value="E501" />
148
- <option value="W29" />
149
- <option value="E501" />
150
- <option value="W29" />
151
- <option value="E501" />
152
- <option value="W29" />
153
- <option value="E501" />
154
- <option value="W29" />
155
- <option value="E501" />
156
- <option value="W29" />
157
- <option value="E501" />
158
- <option value="W29" />
159
- <option value="E501" />
160
- <option value="W29" />
161
- <option value="E501" />
162
- <option value="W29" />
163
- <option value="E501" />
164
- <option value="W29" />
165
- <option value="E501" />
166
- <option value="W29" />
167
- <option value="E501" />
168
- <option value="W29" />
169
- <option value="E501" />
170
- <option value="W29" />
171
- <option value="E501" />
172
- <option value="W29" />
173
- <option value="E501" />
174
- <option value="W29" />
175
- <option value="E501" />
176
- <option value="W29" />
177
- <option value="E501" />
178
- <option value="W29" />
179
- <option value="E501" />
180
- <option value="W29" />
181
- <option value="E501" />
182
- <option value="W29" />
183
- <option value="E501" />
184
- <option value="W29" />
185
- <option value="E501" />
186
- <option value="W29" />
187
- <option value="E501" />
188
- <option value="W29" />
189
- <option value="E501" />
190
- <option value="W29" />
191
- <option value="E501" />
192
- <option value="W29" />
193
- <option value="E501" />
194
- <option value="W29" />
195
- <option value="E501" />
196
- <option value="W29" />
197
- <option value="E501" />
198
- <option value="W29" />
199
- <option value="E501" />
200
- <option value="W29" />
201
- <option value="E501" />
202
- <option value="W29" />
203
- <option value="E501" />
204
- <option value="W29" />
205
- <option value="E501" />
206
- <option value="W29" />
207
- <option value="E501" />
208
- <option value="W29" />
209
- <option value="E501" />
210
- <option value="W29" />
211
- <option value="E501" />
212
- <option value="W29" />
213
- <option value="E501" />
214
- <option value="W29" />
215
- <option value="E501" />
216
- <option value="W29" />
217
- <option value="E501" />
218
- <option value="W29" />
219
- <option value="E501" />
220
- <option value="W29" />
221
- <option value="E501" />
222
- <option value="W29" />
223
- <option value="E501" />
224
- <option value="W29" />
225
- <option value="E501" />
226
- <option value="W29" />
227
- <option value="E501" />
228
- <option value="W29" />
229
- <option value="E501" />
230
- <option value="W29" />
231
- <option value="E501" />
232
- <option value="W29" />
233
- <option value="E501" />
234
- <option value="W29" />
235
- <option value="E501" />
236
- <option value="W29" />
237
- <option value="E501" />
238
- <option value="W29" />
239
- <option value="E501" />
240
- <option value="W29" />
241
- <option value="E501" />
242
- <option value="W29" />
243
- <option value="E501" />
244
- <option value="W29" />
245
- <option value="E501" />
246
- <option value="W29" />
247
- <option value="E501" />
248
- <option value="W29" />
249
- <option value="E501" />
250
- <option value="W29" />
251
- <option value="E501" />
252
- <option value="W29" />
253
- <option value="E501" />
254
- <option value="W29" />
255
- <option value="E501" />
256
- <option value="W29" />
257
- <option value="E501" />
258
- <option value="W29" />
259
- <option value="E501" />
260
- <option value="W29" />
261
- <option value="E501" />
262
- <option value="W29" />
263
- <option value="E501" />
264
- <option value="W29" />
265
- <option value="E501" />
266
- <option value="W29" />
267
- <option value="E501" />
268
- <option value="W29" />
269
- <option value="E501" />
270
- <option value="W29" />
271
- <option value="E501" />
272
- <option value="W29" />
273
- <option value="E501" />
274
- <option value="W29" />
275
- <option value="E501" />
276
- <option value="W29" />
277
- <option value="E501" />
278
- <option value="W29" />
279
- <option value="E501" />
280
- <option value="W29" />
281
- <option value="E501" />
282
- <option value="W29" />
283
- <option value="E501" />
284
- <option value="W29" />
285
- <option value="E501" />
286
- <option value="W29" />
287
- <option value="E501" />
288
- <option value="W29" />
289
- <option value="E501" />
290
- <option value="W29" />
291
- <option value="E501" />
292
- <option value="W29" />
293
- <option value="E501" />
294
- <option value="W29" />
295
- <option value="E501" />
296
- <option value="W29" />
297
- <option value="E501" />
298
- <option value="W29" />
299
- <option value="E501" />
300
- <option value="W29" />
301
- <option value="E501" />
302
- <option value="W29" />
303
- <option value="E501" />
304
- <option value="W29" />
305
- <option value="E501" />
306
- <option value="W29" />
307
- <option value="E501" />
308
- <option value="W29" />
309
- <option value="E501" />
310
- <option value="W29" />
311
- <option value="E501" />
312
- <option value="W29" />
313
- <option value="E501" />
314
- <option value="W29" />
315
- <option value="E501" />
316
- <option value="W29" />
317
- <option value="E501" />
318
- <option value="W29" />
319
- <option value="E501" />
320
- <option value="W29" />
321
- <option value="E501" />
322
- <option value="W29" />
323
- <option value="E501" />
324
- <option value="W29" />
325
- <option value="E501" />
326
- <option value="W29" />
327
- <option value="E501" />
328
- <option value="W29" />
329
- <option value="E501" />
330
- <option value="W29" />
331
- <option value="E501" />
332
- <option value="W29" />
333
- <option value="E501" />
334
- <option value="W29" />
335
- <option value="E501" />
336
- <option value="W29" />
337
- <option value="E501" />
338
- <option value="W29" />
339
- <option value="E501" />
340
- <option value="W29" />
341
- <option value="E501" />
342
- <option value="W29" />
343
- <option value="E501" />
344
- <option value="W29" />
345
- <option value="E501" />
346
- <option value="W29" />
347
- <option value="E501" />
348
- <option value="W29" />
349
- <option value="E501" />
350
- <option value="W29" />
351
- <option value="E501" />
352
- <option value="W29" />
353
- <option value="E501" />
354
- <option value="W29" />
355
- <option value="E501" />
356
- <option value="W29" />
357
- <option value="E501" />
358
- <option value="W29" />
359
- <option value="E501" />
360
- <option value="W29" />
361
- <option value="E501" />
362
- <option value="W29" />
363
- <option value="E501" />
364
- <option value="W29" />
365
- <option value="E501" />
366
- <option value="W29" />
367
- <option value="E501" />
368
- <option value="W29" />
369
- <option value="E501" />
370
- <option value="W29" />
371
- <option value="E501" />
372
- <option value="W29" />
373
- <option value="E501" />
374
- <option value="W29" />
375
- <option value="E501" />
376
- <option value="W29" />
377
- <option value="E501" />
378
- <option value="W29" />
379
- <option value="E501" />
380
- </list>
381
- </option>
382
- </inspection_tool>
383
- <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
384
- <option name="ignoredErrors">
385
- <list>
386
- <option value="N806" />
387
- <option value="N802" />
388
- </list>
389
- </option>
390
- </inspection_tool>
391
- </profile>
392
- </component>
@@ -1,6 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <settings>
3
- <option name="USE_PROJECT_PROFILE" value="false" />
4
- <version value="1.0" />
5
- </settings>
6
- </component>
@@ -1,4 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (pyrox)" project-jdk-type="Python SDK" />
4
- </project>
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/../pyrox-client/.idea/pyrox-client.iml" filepath="$PROJECT_DIR$/../pyrox-client/.idea/pyrox-client.iml" />
6
- </modules>
7
- </component>
8
- </project>
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="PYTHON_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$" />
5
- <orderEntry type="inheritedJdk" />
6
- <orderEntry type="sourceFolder" forTests="false" />
7
- </component>
8
- </module>
@@ -1,191 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AutoImportSettings">
4
- <option name="autoReloadType" value="SELECTIVE" />
5
- </component>
6
- <component name="ChangeListManager">
7
- <list default="true" id="697e0274-d3c5-4401-be7b-acf1d10d3541" name="Changes" comment="" />
8
- <option name="SHOW_DIALOG" value="false" />
9
- <option name="HIGHLIGHT_CONFLICTS" value="true" />
10
- <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
11
- <option name="LAST_RESOLUTION" value="IGNORE" />
12
- </component>
13
- <component name="MarkdownSettingsMigration">
14
- <option name="stateVersion" value="1" />
15
- </component>
16
- <component name="ProjectId" id="31jilXllIHNRHUAEcZyhAlo8CE8" />
17
- <component name="ProjectViewState">
18
- <option name="hideEmptyMiddlePackages" value="true" />
19
- <option name="showLibraryContents" value="true" />
20
- </component>
21
- <component name="PropertiesComponent"><![CDATA[{
22
- "keyToString": {
23
- "RunOnceActivity.OpenProjectViewOnStart": "true",
24
- "RunOnceActivity.ShowReadmeOnStart": "true",
25
- "last_opened_file_path": "/Users/vladmatei"
26
- }
27
- }]]></component>
28
- <component name="RunManager">
29
- <configuration default="true" type="PythonConfigurationType" factoryName="Python">
30
- <module name="pyrox-client" />
31
- <option name="INTERPRETER_OPTIONS" value="" />
32
- <option name="PARENT_ENVS" value="true" />
33
- <envs>
34
- <env name="PYTHONUNBUFFERED" value="1" />
35
- </envs>
36
- <option name="SDK_HOME" value="" />
37
- <option name="WORKING_DIRECTORY" value="" />
38
- <option name="IS_MODULE_SDK" value="false" />
39
- <option name="ADD_CONTENT_ROOTS" value="true" />
40
- <option name="ADD_SOURCE_ROOTS" value="true" />
41
- <option name="SCRIPT_NAME" value="" />
42
- <option name="PARAMETERS" value="" />
43
- <option name="SHOW_COMMAND_LINE" value="false" />
44
- <option name="EMULATE_TERMINAL" value="false" />
45
- <option name="MODULE_MODE" value="false" />
46
- <option name="REDIRECT_INPUT" value="false" />
47
- <option name="INPUT_FILE" value="" />
48
- <method v="2" />
49
- </configuration>
50
- <configuration name="main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
51
- <module name="pyrox-client" />
52
- <option name="INTERPRETER_OPTIONS" value="" />
53
- <option name="PARENT_ENVS" value="true" />
54
- <envs>
55
- <env name="PYTHONUNBUFFERED" value="1" />
56
- </envs>
57
- <option name="SDK_HOME" value="" />
58
- <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
59
- <option name="IS_MODULE_SDK" value="true" />
60
- <option name="ADD_CONTENT_ROOTS" value="true" />
61
- <option name="ADD_SOURCE_ROOTS" value="true" />
62
- <option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
63
- <option name="PARAMETERS" value="" />
64
- <option name="SHOW_COMMAND_LINE" value="false" />
65
- <option name="EMULATE_TERMINAL" value="false" />
66
- <option name="MODULE_MODE" value="false" />
67
- <option name="REDIRECT_INPUT" value="false" />
68
- <option name="INPUT_FILE" value="" />
69
- <method v="2" />
70
- </configuration>
71
- <configuration default="true" type="Tox" factoryName="Tox">
72
- <module name="pyrox-client" />
73
- <option name="INTERPRETER_OPTIONS" value="" />
74
- <option name="PARENT_ENVS" value="true" />
75
- <option name="SDK_HOME" value="" />
76
- <option name="WORKING_DIRECTORY" value="" />
77
- <option name="IS_MODULE_SDK" value="false" />
78
- <option name="ADD_CONTENT_ROOTS" value="true" />
79
- <option name="ADD_SOURCE_ROOTS" value="true" />
80
- <method v="2" />
81
- </configuration>
82
- <configuration default="true" type="ccruntests" factoryName="Run Study Tests">
83
- <module name="pyrox-client" />
84
- <option name="INTERPRETER_OPTIONS" value="" />
85
- <option name="PARENT_ENVS" value="true" />
86
- <envs>
87
- <env name="PYTHONUNBUFFERED" value="1" />
88
- </envs>
89
- <option name="SDK_HOME" value="" />
90
- <option name="WORKING_DIRECTORY" value="" />
91
- <option name="IS_MODULE_SDK" value="false" />
92
- <option name="ADD_CONTENT_ROOTS" value="true" />
93
- <option name="ADD_SOURCE_ROOTS" value="true" />
94
- <option name="SCRIPT_NAME" value="" />
95
- <option name="PARAMETERS" value="" />
96
- <option name="SHOW_COMMAND_LINE" value="false" />
97
- <option name="EMULATE_TERMINAL" value="false" />
98
- <option name="MODULE_MODE" value="false" />
99
- <option name="REDIRECT_INPUT" value="false" />
100
- <option name="INPUT_FILE" value="" />
101
- <option name="studyTest" value="" />
102
- <method v="2" />
103
- </configuration>
104
- <configuration default="true" type="docs" factoryName="Docutils task">
105
- <module name="pyrox-client" />
106
- <option name="INTERPRETER_OPTIONS" value="" />
107
- <option name="PARENT_ENVS" value="true" />
108
- <option name="SDK_HOME" value="" />
109
- <option name="WORKING_DIRECTORY" value="" />
110
- <option name="IS_MODULE_SDK" value="false" />
111
- <option name="ADD_CONTENT_ROOTS" value="true" />
112
- <option name="ADD_SOURCE_ROOTS" value="true" />
113
- <option name="docutils_input_file" value="" />
114
- <option name="docutils_output_file" value="" />
115
- <option name="docutils_params" value="" />
116
- <option name="docutils_task" value="" />
117
- <option name="docutils_open_in_browser" value="false" />
118
- <method v="2" />
119
- </configuration>
120
- <configuration default="true" type="docs" factoryName="Sphinx task">
121
- <module name="pyrox-client" />
122
- <option name="INTERPRETER_OPTIONS" value="" />
123
- <option name="PARENT_ENVS" value="true" />
124
- <option name="SDK_HOME" value="" />
125
- <option name="WORKING_DIRECTORY" value="" />
126
- <option name="IS_MODULE_SDK" value="false" />
127
- <option name="ADD_CONTENT_ROOTS" value="true" />
128
- <option name="ADD_SOURCE_ROOTS" value="true" />
129
- <option name="docutils_input_file" value="" />
130
- <option name="docutils_output_file" value="" />
131
- <option name="docutils_params" value="" />
132
- <option name="docutils_task" value="" />
133
- <option name="docutils_open_in_browser" value="false" />
134
- <method v="2" />
135
- </configuration>
136
- <configuration default="true" type="tests" factoryName="Autodetect">
137
- <module name="pyrox-client" />
138
- <option name="INTERPRETER_OPTIONS" value="" />
139
- <option name="PARENT_ENVS" value="true" />
140
- <option name="SDK_HOME" value="" />
141
- <option name="WORKING_DIRECTORY" value="" />
142
- <option name="IS_MODULE_SDK" value="false" />
143
- <option name="ADD_CONTENT_ROOTS" value="true" />
144
- <option name="ADD_SOURCE_ROOTS" value="true" />
145
- <option name="_new_additionalArguments" value="&quot;&quot;" />
146
- <option name="_new_target" value="&quot;&quot;" />
147
- <option name="_new_targetType" value="&quot;PATH&quot;" />
148
- <method v="2" />
149
- </configuration>
150
- <configuration default="true" type="tests" factoryName="Doctests">
151
- <module name="pyrox-client" />
152
- <option name="INTERPRETER_OPTIONS" value="" />
153
- <option name="PARENT_ENVS" value="true" />
154
- <option name="SDK_HOME" value="" />
155
- <option name="WORKING_DIRECTORY" value="" />
156
- <option name="IS_MODULE_SDK" value="false" />
157
- <option name="ADD_CONTENT_ROOTS" value="true" />
158
- <option name="ADD_SOURCE_ROOTS" value="true" />
159
- <option name="SCRIPT_NAME" value="" />
160
- <option name="CLASS_NAME" value="" />
161
- <option name="METHOD_NAME" value="" />
162
- <option name="FOLDER_NAME" value="" />
163
- <option name="TEST_TYPE" value="TEST_SCRIPT" />
164
- <option name="PATTERN" value="" />
165
- <option name="USE_PATTERN" value="false" />
166
- <method v="2" />
167
- </configuration>
168
- </component>
169
- <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
170
- <component name="TaskManager">
171
- <task active="true" id="Default" summary="Default task">
172
- <changelist id="697e0274-d3c5-4401-be7b-acf1d10d3541" name="Changes" comment="" />
173
- <created>1756044033429</created>
174
- <option name="number" value="Default" />
175
- <option name="presentableId" value="Default" />
176
- <updated>1756044033429</updated>
177
- </task>
178
- <servers />
179
- </component>
180
- <component name="XDebuggerManager">
181
- <breakpoint-manager>
182
- <breakpoints>
183
- <line-breakpoint enabled="true" suspend="THREAD" type="python-line">
184
- <url>file://$PROJECT_DIR$/../pyrox-client/main.py</url>
185
- <line>8</line>
186
- <option name="timeStamp" value="1" />
187
- </line-breakpoint>
188
- </breakpoints>
189
- </breakpoint-manager>
190
- </component>
191
- </project>
@@ -1,16 +0,0 @@
1
- # This is a sample Python script.
2
-
3
- # Press ⌃R to execute it or replace it with your code.
4
- # Press Double ⇧ to search everywhere for classes, files, tool windows, actions, and settings.
5
-
6
-
7
- def print_hi(name):
8
- # Use a breakpoint in the code line below to debug your script.
9
- print(f'Hi, {name}') # Press ⌘F8 to toggle the breakpoint.
10
-
11
-
12
- # Press the green button in the gutter to run the script.
13
- if __name__ == '__main__':
14
- print_hi('PyCharm')
15
-
16
- # See PyCharm help at https://www.jetbrains.com/help/pycharm/
@@ -1,23 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling>=1.20"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "pyrox-client"
7
- version = "0.0.1"
8
- description = "HYROX race data client - retrieve full race results via Python"
9
- authors = [{name="Vlad Matei"}]
10
- readme = "README.md"
11
- license = {text = "MIT"}
12
- requires-python = ">=3.9"
13
- dependencies = ["pandas>=2.0", "fastparquet>=15", "requests>=2.31"]
14
-
15
- [project.optional-dependencies]
16
- duckdb = ["duckdb>=1.0.0"]
17
-
18
- [project.urls]
19
- homepage = "https://github.com/<you>/pyrox"
20
-
21
- [tool.hatch.build.targets.wheel]
22
- packages = ["src/pyrox"] # import package path
23
-
@@ -1 +0,0 @@
1
- *
@@ -1 +0,0 @@
1
- __version__ = "0.0.1"
@@ -1,49 +0,0 @@
1
- import pandas as pd
2
- import io, json, requests
3
- from dataclasses import dataclass
4
-
5
- CDN_BASE = "https://<your-cloudfront-domain>/<prefix>"
6
-
7
-
8
- @dataclass(frozen=True)
9
- class EventRef:
10
- season: int
11
- event_id: str
12
- race: str
13
- key: str # "season=7/Cardiff_2025__LR3MS4JIB56.parquet"
14
-
15
-
16
- def _manifest():
17
- url = f"{CDN_BASE}/catalog/manifest-v1.json"
18
- r = requests.get(url, timeout=20)
19
- r.raise_for_status()
20
- return r.json()
21
-
22
-
23
- def list_seasons() -> list[int]:
24
- return _manifest()["seasons"]
25
-
26
-
27
- def list_events(season: int | None = None) -> list[EventRef]:
28
- m = _manifest()["events"]
29
- if season is not None:
30
- m = [e for e in m if e["season"] == season]
31
- return [EventRef(**{k: e[k] for k in ("season", "event_id", "race", "key")}) for e in m]
32
-
33
-
34
- def get_race(*, season: int | None = None, location: str | None = None, event_id: str | None = None) -> pd.DataFrame:
35
- events = list_events(season)
36
- if event_id:
37
- ev = next(e for e in events if e.event_id == event_id)
38
- elif location:
39
- # naive match; you can add fuzzy/normalized matching later
40
- cand = [e for e in events if location.lower() in e.race.lower()]
41
- if not cand:
42
- raise ValueError(f"No event matching {location!r}")
43
- ev = cand[0]
44
- else:
45
- raise ValueError("Provide event_id or location (optionally season).")
46
- url = f"{CDN_BASE}/{ev.key}"
47
- resp = requests.get(url, timeout=60)
48
- resp.raise_for_status()
49
- return pd.read_parquet(io.BytesIO(resp.content))
File without changes