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.
Files changed (50) hide show
  1. orbitoby/__init__.py +11 -0
  2. orbitoby/archive/__init__.py +0 -0
  3. orbitoby/archive/raw.py +94 -0
  4. orbitoby/auth.py +320 -0
  5. orbitoby/canonical.py +1157 -0
  6. orbitoby/catalogue.py +1096 -0
  7. orbitoby/cli.py +0 -0
  8. orbitoby/config.py +35 -0
  9. orbitoby/http.py +315 -0
  10. orbitoby/models/__init__.py +11 -0
  11. orbitoby/models/msis.py +547 -0
  12. orbitoby/plotting.py +563 -0
  13. orbitoby/research.py +906 -0
  14. orbitoby/service.py +1496 -0
  15. orbitoby/settings.py +258 -0
  16. orbitoby/source_api.py +816 -0
  17. orbitoby/sources/__init__.py +21 -0
  18. orbitoby/sources/base.py +122 -0
  19. orbitoby/sources/cdaweb.py +33 -0
  20. orbitoby/sources/celestrak.py +158 -0
  21. orbitoby/sources/declarative.py +225 -0
  22. orbitoby/sources/discos.py +249 -0
  23. orbitoby/sources/donki.py +315 -0
  24. orbitoby/sources/gcat.py +233 -0
  25. orbitoby/sources/gfz.py +331 -0
  26. orbitoby/sources/hapi.py +496 -0
  27. orbitoby/sources/latis.py +385 -0
  28. orbitoby/sources/launchlibrary.py +116 -0
  29. orbitoby/sources/lisird.py +168 -0
  30. orbitoby/sources/metadata.py +857 -0
  31. orbitoby/sources/noaa.py +195 -0
  32. orbitoby/sources/nrcan.py +638 -0
  33. orbitoby/sources/plugins.py +96 -0
  34. orbitoby/sources/registry.py +137 -0
  35. orbitoby/sources/satnogs.py +151 -0
  36. orbitoby/sources/silso.py +262 -0
  37. orbitoby/sources/spacetrack.py +256 -0
  38. orbitoby/sources/swarm.py +54 -0
  39. orbitoby/sources/wdc_kyoto.py +33 -0
  40. orbitoby/warehouse/__init__.py +0 -0
  41. orbitoby/warehouse/coverage.py +87 -0
  42. orbitoby/warehouse/db.py +89 -0
  43. orbitoby/warehouse/identity.py +600 -0
  44. orbitoby/warehouse/products.py +949 -0
  45. orbitoby/warehouse/scientific.py +1543 -0
  46. orbitoby-0.1.0.dist-info/METADATA +181 -0
  47. orbitoby-0.1.0.dist-info/RECORD +50 -0
  48. orbitoby-0.1.0.dist-info/WHEEL +4 -0
  49. orbitoby-0.1.0.dist-info/licenses/LICENSE +202 -0
  50. orbitoby-0.1.0.dist-info/licenses/NOTICE +15 -0
orbitoby/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from orbitoby.research import ResearchWindow
2
+ from orbitoby.service import Archive
3
+ from orbitoby.warehouse.products import (
4
+ ProductParent,
5
+ )
6
+
7
+ __all__ = [
8
+ "Archive",
9
+ "ProductParent",
10
+ "ResearchWindow",
11
+ ]
File without changes
@@ -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)