orbitoby 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orbitoby/__init__.py +11 -0
- orbitoby/archive/__init__.py +0 -0
- orbitoby/archive/raw.py +94 -0
- orbitoby/auth.py +320 -0
- orbitoby/canonical.py +1157 -0
- orbitoby/catalogue.py +1096 -0
- orbitoby/cli.py +0 -0
- orbitoby/config.py +35 -0
- orbitoby/http.py +315 -0
- orbitoby/models/__init__.py +11 -0
- orbitoby/models/msis.py +547 -0
- orbitoby/plotting.py +563 -0
- orbitoby/research.py +906 -0
- orbitoby/service.py +1496 -0
- orbitoby/settings.py +258 -0
- orbitoby/source_api.py +816 -0
- orbitoby/sources/__init__.py +21 -0
- orbitoby/sources/base.py +122 -0
- orbitoby/sources/cdaweb.py +33 -0
- orbitoby/sources/celestrak.py +158 -0
- orbitoby/sources/declarative.py +225 -0
- orbitoby/sources/discos.py +249 -0
- orbitoby/sources/donki.py +315 -0
- orbitoby/sources/gcat.py +233 -0
- orbitoby/sources/gfz.py +331 -0
- orbitoby/sources/hapi.py +496 -0
- orbitoby/sources/latis.py +385 -0
- orbitoby/sources/launchlibrary.py +116 -0
- orbitoby/sources/lisird.py +168 -0
- orbitoby/sources/metadata.py +857 -0
- orbitoby/sources/noaa.py +195 -0
- orbitoby/sources/nrcan.py +638 -0
- orbitoby/sources/plugins.py +96 -0
- orbitoby/sources/registry.py +137 -0
- orbitoby/sources/satnogs.py +151 -0
- orbitoby/sources/silso.py +262 -0
- orbitoby/sources/spacetrack.py +256 -0
- orbitoby/sources/swarm.py +54 -0
- orbitoby/sources/wdc_kyoto.py +33 -0
- orbitoby/warehouse/__init__.py +0 -0
- orbitoby/warehouse/coverage.py +87 -0
- orbitoby/warehouse/db.py +89 -0
- orbitoby/warehouse/identity.py +600 -0
- orbitoby/warehouse/products.py +949 -0
- orbitoby/warehouse/scientific.py +1543 -0
- orbitoby-0.1.0.dist-info/METADATA +181 -0
- orbitoby-0.1.0.dist-info/RECORD +50 -0
- orbitoby-0.1.0.dist-info/WHEEL +4 -0
- orbitoby-0.1.0.dist-info/licenses/LICENSE +202 -0
- orbitoby-0.1.0.dist-info/licenses/NOTICE +15 -0
orbitoby/__init__.py
ADDED
|
File without changes
|
orbitoby/archive/raw.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, date, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from orbitoby.config import RAW_DIR
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class RawArtifact:
|
|
15
|
+
artifact_id: str
|
|
16
|
+
source: str
|
|
17
|
+
dataset: str
|
|
18
|
+
norad_id: int | None
|
|
19
|
+
requested_start: date | None
|
|
20
|
+
requested_end: date | None
|
|
21
|
+
retrieved_at: datetime
|
|
22
|
+
sha256: str
|
|
23
|
+
path: Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_raw_artifact(
|
|
27
|
+
*,
|
|
28
|
+
source: str,
|
|
29
|
+
dataset: str,
|
|
30
|
+
payload: bytes,
|
|
31
|
+
norad_id: int | None = None,
|
|
32
|
+
start: date | None = None,
|
|
33
|
+
end: date | None = None,
|
|
34
|
+
metadata: dict | None = None,
|
|
35
|
+
extension: str = "json",
|
|
36
|
+
) -> RawArtifact:
|
|
37
|
+
retrieved_at = datetime.now(UTC)
|
|
38
|
+
|
|
39
|
+
# 원본 파일이 같은지 확인할 수 있는 fingerprint
|
|
40
|
+
sha256 = hashlib.sha256(payload).hexdigest()
|
|
41
|
+
|
|
42
|
+
artifact_id = f"{retrieved_at:%Y%m%dT%H%M%SZ}_{sha256[:12]}_{uuid4().hex}"
|
|
43
|
+
|
|
44
|
+
object_dir = f"norad={norad_id}" if norad_id is not None else "global"
|
|
45
|
+
|
|
46
|
+
year_dir = str(start.year) if start is not None else str(retrieved_at.year)
|
|
47
|
+
|
|
48
|
+
directory = RAW_DIR / source / dataset / object_dir / year_dir
|
|
49
|
+
|
|
50
|
+
directory.mkdir(
|
|
51
|
+
parents=True,
|
|
52
|
+
exist_ok=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
payload_path = directory / f"{artifact_id}.{extension}"
|
|
56
|
+
|
|
57
|
+
# API 원본 그대로 저장
|
|
58
|
+
payload_path.write_bytes(payload)
|
|
59
|
+
|
|
60
|
+
manifest_path = directory / f"{artifact_id}.manifest.json"
|
|
61
|
+
|
|
62
|
+
manifest = {
|
|
63
|
+
"artifact_id": artifact_id,
|
|
64
|
+
"source": source,
|
|
65
|
+
"dataset": dataset,
|
|
66
|
+
"norad_id": norad_id,
|
|
67
|
+
"requested_start": start.isoformat() if start else None,
|
|
68
|
+
"requested_end": end.isoformat() if end else None,
|
|
69
|
+
"retrieved_at": retrieved_at.isoformat(),
|
|
70
|
+
"sha256": sha256,
|
|
71
|
+
"payload_path": str(payload_path),
|
|
72
|
+
"metadata": metadata or {},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
manifest_path.write_text(
|
|
76
|
+
json.dumps(
|
|
77
|
+
manifest,
|
|
78
|
+
ensure_ascii=False,
|
|
79
|
+
indent=2,
|
|
80
|
+
),
|
|
81
|
+
encoding="utf-8",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return RawArtifact(
|
|
85
|
+
artifact_id=artifact_id,
|
|
86
|
+
source=source,
|
|
87
|
+
dataset=dataset,
|
|
88
|
+
norad_id=norad_id,
|
|
89
|
+
requested_start=start,
|
|
90
|
+
requested_end=end,
|
|
91
|
+
retrieved_at=retrieved_at,
|
|
92
|
+
sha256=sha256,
|
|
93
|
+
path=payload_path,
|
|
94
|
+
)
|
orbitoby/auth.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from dotenv import dotenv_values
|
|
9
|
+
|
|
10
|
+
CredentialSource = Literal[
|
|
11
|
+
"runtime",
|
|
12
|
+
"keyring",
|
|
13
|
+
"environment",
|
|
14
|
+
"colab",
|
|
15
|
+
"dotenv",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class CredentialResult:
|
|
21
|
+
value: str
|
|
22
|
+
source: CredentialSource
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CredentialManager:
|
|
26
|
+
"""Resolve user-owned credentials without exposing secret values.
|
|
27
|
+
|
|
28
|
+
Resolution order:
|
|
29
|
+
explicit runtime
|
|
30
|
+
→ OS keyring
|
|
31
|
+
→ environment
|
|
32
|
+
→ Colab Secrets
|
|
33
|
+
→ explicit development .env fallback
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
*,
|
|
39
|
+
runtime: dict[tuple[str, str], str] | None = None,
|
|
40
|
+
allow_dotenv: bool = False,
|
|
41
|
+
dotenv_path: str | Path | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._runtime = dict(runtime or {})
|
|
44
|
+
self.allow_dotenv = allow_dotenv
|
|
45
|
+
|
|
46
|
+
if dotenv_path is None:
|
|
47
|
+
dotenv_path = Path(__file__).resolve().parents[2] / ".env"
|
|
48
|
+
|
|
49
|
+
self.dotenv_path = Path(dotenv_path)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _normalize(
|
|
53
|
+
provider: str,
|
|
54
|
+
key: str,
|
|
55
|
+
) -> tuple[str, str]:
|
|
56
|
+
provider = provider.strip().lower()
|
|
57
|
+
key = key.strip().lower()
|
|
58
|
+
|
|
59
|
+
if not provider or not key:
|
|
60
|
+
raise ValueError("provider and key must be non-empty")
|
|
61
|
+
|
|
62
|
+
return provider, key
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def env_name(
|
|
66
|
+
provider: str,
|
|
67
|
+
key: str,
|
|
68
|
+
) -> str:
|
|
69
|
+
provider, key = CredentialManager._normalize(
|
|
70
|
+
provider,
|
|
71
|
+
key,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return f"{provider}_{key}".replace("-", "_").upper()
|
|
75
|
+
|
|
76
|
+
def set_runtime(
|
|
77
|
+
self,
|
|
78
|
+
provider: str,
|
|
79
|
+
key: str,
|
|
80
|
+
value: str,
|
|
81
|
+
) -> None:
|
|
82
|
+
provider, key = self._normalize(
|
|
83
|
+
provider,
|
|
84
|
+
key,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if not value:
|
|
88
|
+
raise ValueError("credential value must not be empty")
|
|
89
|
+
|
|
90
|
+
self._runtime[(provider, key)] = value
|
|
91
|
+
|
|
92
|
+
def clear_runtime(
|
|
93
|
+
self,
|
|
94
|
+
provider: str,
|
|
95
|
+
key: str,
|
|
96
|
+
) -> None:
|
|
97
|
+
provider, key = self._normalize(
|
|
98
|
+
provider,
|
|
99
|
+
key,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self._runtime.pop(
|
|
103
|
+
(provider, key),
|
|
104
|
+
None,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _from_keyring(
|
|
109
|
+
provider: str,
|
|
110
|
+
key: str,
|
|
111
|
+
) -> str | None:
|
|
112
|
+
try:
|
|
113
|
+
import keyring
|
|
114
|
+
except ImportError:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return keyring.get_password(
|
|
119
|
+
f"orbitoby:{provider}",
|
|
120
|
+
key,
|
|
121
|
+
)
|
|
122
|
+
except Exception: # noqa: BLE001
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _from_colab(
|
|
127
|
+
env_name: str,
|
|
128
|
+
) -> str | None:
|
|
129
|
+
try:
|
|
130
|
+
from google.colab import userdata
|
|
131
|
+
except ImportError:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
value = userdata.get(env_name)
|
|
136
|
+
except Exception: # noqa: BLE001
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
return value or None
|
|
140
|
+
|
|
141
|
+
def _from_dotenv(
|
|
142
|
+
self,
|
|
143
|
+
env_name: str,
|
|
144
|
+
) -> str | None:
|
|
145
|
+
if not self.allow_dotenv or not self.dotenv_path.exists():
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
value = dotenv_values(self.dotenv_path).get(env_name)
|
|
149
|
+
|
|
150
|
+
return str(value) if value else None
|
|
151
|
+
|
|
152
|
+
def resolve(
|
|
153
|
+
self,
|
|
154
|
+
provider: str,
|
|
155
|
+
key: str,
|
|
156
|
+
*,
|
|
157
|
+
env_name: str | None = None,
|
|
158
|
+
) -> CredentialResult | None:
|
|
159
|
+
provider, key = self._normalize(
|
|
160
|
+
provider,
|
|
161
|
+
key,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
runtime = self._runtime.get((provider, key))
|
|
165
|
+
|
|
166
|
+
if runtime:
|
|
167
|
+
return CredentialResult(
|
|
168
|
+
runtime,
|
|
169
|
+
"runtime",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
keyring_value = self._from_keyring(
|
|
173
|
+
provider,
|
|
174
|
+
key,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if keyring_value:
|
|
178
|
+
return CredentialResult(
|
|
179
|
+
keyring_value,
|
|
180
|
+
"keyring",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
env_name = env_name or self.env_name(
|
|
184
|
+
provider,
|
|
185
|
+
key,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
environment = os.getenv(env_name)
|
|
189
|
+
|
|
190
|
+
if environment:
|
|
191
|
+
return CredentialResult(
|
|
192
|
+
environment,
|
|
193
|
+
"environment",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
colab = self._from_colab(env_name)
|
|
197
|
+
|
|
198
|
+
if colab:
|
|
199
|
+
return CredentialResult(
|
|
200
|
+
colab,
|
|
201
|
+
"colab",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
dotenv = self._from_dotenv(env_name)
|
|
205
|
+
|
|
206
|
+
if dotenv:
|
|
207
|
+
return CredentialResult(
|
|
208
|
+
dotenv,
|
|
209
|
+
"dotenv",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def get(
|
|
215
|
+
self,
|
|
216
|
+
provider: str,
|
|
217
|
+
key: str,
|
|
218
|
+
*,
|
|
219
|
+
env_name: str | None = None,
|
|
220
|
+
required: bool = False,
|
|
221
|
+
) -> str | None:
|
|
222
|
+
result = self.resolve(
|
|
223
|
+
provider,
|
|
224
|
+
key,
|
|
225
|
+
env_name=env_name,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if result is None:
|
|
229
|
+
if required:
|
|
230
|
+
raise RuntimeError(f"Credential not configured: {provider}/{key}")
|
|
231
|
+
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
return result.value
|
|
235
|
+
|
|
236
|
+
def status(
|
|
237
|
+
self,
|
|
238
|
+
provider: str,
|
|
239
|
+
keys: tuple[str, ...],
|
|
240
|
+
) -> dict[str, dict[str, object]]:
|
|
241
|
+
report = {}
|
|
242
|
+
|
|
243
|
+
for key in keys:
|
|
244
|
+
result = self.resolve(
|
|
245
|
+
provider,
|
|
246
|
+
key,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
report[key] = {
|
|
250
|
+
"configured": (result is not None),
|
|
251
|
+
"source": (result.source if result else None),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return report
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def set_keyring(
|
|
258
|
+
provider: str,
|
|
259
|
+
key: str,
|
|
260
|
+
value: str,
|
|
261
|
+
) -> None:
|
|
262
|
+
try:
|
|
263
|
+
import keyring
|
|
264
|
+
except ImportError as exc:
|
|
265
|
+
raise RuntimeError(
|
|
266
|
+
"Install the optional 'keyring' package to store OS credentials."
|
|
267
|
+
) from exc
|
|
268
|
+
|
|
269
|
+
keyring.set_password(
|
|
270
|
+
f"orbitoby:{provider}",
|
|
271
|
+
key,
|
|
272
|
+
value,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def delete_keyring(
|
|
277
|
+
provider: str,
|
|
278
|
+
key: str,
|
|
279
|
+
) -> None:
|
|
280
|
+
try:
|
|
281
|
+
import keyring
|
|
282
|
+
except ImportError as exc:
|
|
283
|
+
raise RuntimeError(
|
|
284
|
+
"Install the optional 'keyring' package to manage OS credentials."
|
|
285
|
+
) from exc
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
keyring.delete_password(
|
|
289
|
+
f"orbitoby:{provider}",
|
|
290
|
+
key,
|
|
291
|
+
)
|
|
292
|
+
except keyring.errors.PasswordDeleteError:
|
|
293
|
+
# Deleting an already-absent credential is idempotent.
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
_TRUE_VALUES = frozenset(
|
|
298
|
+
{
|
|
299
|
+
"1",
|
|
300
|
+
"true",
|
|
301
|
+
"yes",
|
|
302
|
+
"on",
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def default_credential_manager() -> CredentialManager:
|
|
308
|
+
"""Create Orbitoby's default credential resolver.
|
|
309
|
+
|
|
310
|
+
Local .env loading is intentionally opt-in through
|
|
311
|
+
ORBITOBY_ALLOW_DOTENV=1. Importing Orbitoby never loads .env.
|
|
312
|
+
"""
|
|
313
|
+
raw = os.getenv(
|
|
314
|
+
"ORBITOBY_ALLOW_DOTENV",
|
|
315
|
+
"",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
allow_dotenv = raw.strip().lower() in _TRUE_VALUES
|
|
319
|
+
|
|
320
|
+
return CredentialManager(allow_dotenv=allow_dotenv)
|