financechatbotkit 2.0.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 (39) hide show
  1. financechatbotkit-2.0.0.dist-info/METADATA +11 -0
  2. financechatbotkit-2.0.0.dist-info/RECORD +39 -0
  3. financechatbotkit-2.0.0.dist-info/WHEEL +5 -0
  4. financechatbotkit-2.0.0.dist-info/entry_points.txt +2 -0
  5. financechatbotkit-2.0.0.dist-info/top_level.txt +2 -0
  6. orchestrator/__init__.py +29 -0
  7. orchestrator/bond/__init__.py +8 -0
  8. orchestrator/bond/base_reader.py +139 -0
  9. orchestrator/bond/getBondBasiInfo.py +84 -0
  10. orchestrator/bond/getBondWithOptiCallRede.py +83 -0
  11. orchestrator/bond/getEarlExerOpti.py +90 -0
  12. orchestrator/bond/getIssuIssuItemStat.py +85 -0
  13. orchestrator/bond/getOptiExer.py +83 -0
  14. orchestrator/bond/getOptiExerPricAdju.py +84 -0
  15. orchestrator/bond/workflow.py +252 -0
  16. orchestrator/exceptions.py +17 -0
  17. orchestrator/fnguide/__init__.py +21 -0
  18. orchestrator/fnguide/workflow.py +391 -0
  19. orchestrator/mapping/__init__.py +22 -0
  20. orchestrator/mapping/data/__init__.py +1 -0
  21. orchestrator/mapping/data/corp_codes_raw.json +693170 -0
  22. orchestrator/mapping/update_raw_data.py +96 -0
  23. orchestrator/mapping/workflow.py +303 -0
  24. orchestrator/price/__init__.py +15 -0
  25. orchestrator/price/workflow.py +250 -0
  26. telebotkit/__init__.py +51 -0
  27. telebotkit/bot/__init__.py +38 -0
  28. telebotkit/bot/client.py +217 -0
  29. telebotkit/bot/reply.py +36 -0
  30. telebotkit/bot/router.py +125 -0
  31. telebotkit/bot/safety.py +28 -0
  32. telebotkit/bot/telegram.py +41 -0
  33. telebotkit/firestore/__init__.py +45 -0
  34. telebotkit/firestore/client.py +141 -0
  35. telebotkit/firestore/documents.py +164 -0
  36. telebotkit/firestore/fetch.py +228 -0
  37. telebotkit/firestore/locks.py +74 -0
  38. telebotkit/firestore/upload.py +75 -0
  39. telebotkit/sheets.py +219 -0
@@ -0,0 +1,141 @@
1
+ """Shared Firestore client bootstrap and error helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ _SERVICE_ACCOUNT_BASE_FIELDS = {
13
+ "type": "service_account",
14
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
15
+ "token_uri": "https://oauth2.googleapis.com/token",
16
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
17
+ }
18
+
19
+
20
+ class FirestoreQuotaError(Exception):
21
+ """Raised when a Firestore operation fails due to quota or write limits."""
22
+
23
+
24
+ def raise_if_quota_error(error: Exception) -> None:
25
+ """Re-raise quota-related Google API exceptions in a stable app-level form."""
26
+
27
+ try:
28
+ from google.api_core import exceptions as google_api_exceptions # type: ignore
29
+
30
+ if isinstance(error, google_api_exceptions.ResourceExhausted):
31
+ raise FirestoreQuotaError(str(error)) from error
32
+ except ImportError:
33
+ return
34
+
35
+
36
+ def load_service_account_info_from_env() -> dict[str, Any] | None:
37
+ """Construct service-account credentials from discrete GOOGLE_* env vars."""
38
+
39
+ client_email = os.environ.get("GOOGLE_CLIENT_EMAIL", "").strip()
40
+ if not client_email:
41
+ return None
42
+
43
+ private_key = os.environ.get("GOOGLE_PRIVATE_KEY", "").strip()
44
+ if "\\n" in private_key and "\n" not in private_key:
45
+ private_key = private_key.replace("\\n", "\n")
46
+
47
+ return {
48
+ **_SERVICE_ACCOUNT_BASE_FIELDS,
49
+ "project_id": os.environ.get("GOOGLE_PROJECT_ID", "").strip(),
50
+ "private_key_id": os.environ.get("GOOGLE_PRIVATE_KEY_ID", "").strip(),
51
+ "private_key": private_key,
52
+ "client_email": client_email,
53
+ "client_id": os.environ.get("GOOGLE_CLIENT_ID", "").strip(),
54
+ "client_x509_cert_url": os.environ.get("GOOGLE_CLIENT_X509_CERT_URL", "").strip(),
55
+ }
56
+
57
+
58
+ class FirestoreClientProvider:
59
+ """Lazily create and cache a Firestore client."""
60
+
61
+ def __init__(self) -> None:
62
+ self._client = None
63
+
64
+ def reset(self) -> None:
65
+ self._client = None
66
+
67
+ def get_client(self):
68
+ if self._client is None:
69
+ project_id = (
70
+ os.environ.get("GOOGLE_PROJECT_ID")
71
+ or os.environ.get("FIRESTORE_PROJECT_ID")
72
+ or None
73
+ )
74
+
75
+ from google.cloud import firestore # type: ignore
76
+
77
+ service_account_info = load_service_account_info_from_env()
78
+ if service_account_info:
79
+ from google.oauth2 import service_account # type: ignore
80
+
81
+ credentials = service_account.Credentials.from_service_account_info(
82
+ service_account_info,
83
+ scopes=["https://www.googleapis.com/auth/datastore"],
84
+ )
85
+ self._client = firestore.Client(
86
+ project=project_id or service_account_info.get("project_id") or None,
87
+ credentials=credentials,
88
+ )
89
+ logger.info("Firestore client created from individual GOOGLE_* env vars.")
90
+ else:
91
+ credentials_json = os.environ.get(
92
+ "GOOGLE_APPLICATION_CREDENTIALS_JSON",
93
+ "",
94
+ ).strip()
95
+ if credentials_json:
96
+ from google.oauth2 import service_account # type: ignore
97
+
98
+ info = json.loads(credentials_json)
99
+ credentials = service_account.Credentials.from_service_account_info(
100
+ info,
101
+ scopes=["https://www.googleapis.com/auth/datastore"],
102
+ )
103
+ self._client = firestore.Client(
104
+ project=project_id,
105
+ credentials=credentials,
106
+ )
107
+ logger.info(
108
+ "Firestore client created from GOOGLE_APPLICATION_CREDENTIALS_JSON."
109
+ )
110
+ else:
111
+ self._client = firestore.Client(project=project_id)
112
+ logger.info(
113
+ "Firestore client created from Application Default Credentials."
114
+ )
115
+ return self._client
116
+
117
+ FirestoreClientFactory = FirestoreClientProvider
118
+
119
+ _DEFAULT_CLIENT_PROVIDER = FirestoreClientProvider()
120
+
121
+
122
+ def get_client():
123
+ """Return the shared cached Firestore client."""
124
+
125
+ return _DEFAULT_CLIENT_PROVIDER.get_client()
126
+
127
+
128
+ def reset_client() -> None:
129
+ """Reset the shared cached Firestore client."""
130
+
131
+ _DEFAULT_CLIENT_PROVIDER.reset()
132
+
133
+ __all__ = [
134
+ "FirestoreClientFactory",
135
+ "FirestoreClientProvider",
136
+ "FirestoreQuotaError",
137
+ "get_client",
138
+ "load_service_account_info_from_env",
139
+ "raise_if_quota_error",
140
+ "reset_client",
141
+ ]
@@ -0,0 +1,164 @@
1
+ """Typed Firestore repository helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any, Generic, TypeVar
7
+
8
+ from telebotkit.firestore.client import get_client
9
+ from telebotkit.firestore.fetch import FetchResult, fetch_document, invalidate_document_cache
10
+
11
+ DocumentT = TypeVar("DocumentT")
12
+
13
+
14
+ class DocumentStore(Generic[DocumentT]):
15
+ """Typed access to a Firestore collection with explicit document IDs."""
16
+
17
+ def __init__(
18
+ self,
19
+ *,
20
+ collection_name: str,
21
+ from_dict: Callable[[dict[str, Any]], DocumentT],
22
+ to_dict: Callable[[DocumentT], dict[str, Any]],
23
+ client_provider: Callable[[], Any] = get_client,
24
+ cache_ttl_seconds: float | None = None,
25
+ ) -> None:
26
+ self._collection_name = collection_name
27
+ self._from_dict = from_dict
28
+ self._to_dict = to_dict
29
+ self._client_provider = client_provider
30
+ self._cache_ttl_seconds = cache_ttl_seconds
31
+
32
+ def get_result(
33
+ self,
34
+ document_id: str,
35
+ *,
36
+ force_refresh: bool = False,
37
+ ) -> FetchResult[DocumentT]:
38
+ return fetch_document(
39
+ self._collection_name,
40
+ document_id,
41
+ parse=self._from_dict,
42
+ client_provider=self._client_provider,
43
+ cache_ttl_seconds=self._cache_ttl_seconds,
44
+ force_refresh=force_refresh,
45
+ )
46
+
47
+ def get(
48
+ self,
49
+ document_id: str,
50
+ *,
51
+ force_refresh: bool = False,
52
+ ) -> DocumentT | None:
53
+ result = self.get_result(document_id, force_refresh=force_refresh)
54
+ if result.error is not None:
55
+ raise result.error
56
+ return result.data
57
+
58
+ def get_or_create(
59
+ self,
60
+ document_id: str,
61
+ factory: Callable[[], DocumentT],
62
+ ) -> DocumentT:
63
+ document = self.get(document_id)
64
+ return document if document is not None else factory()
65
+
66
+ def save(
67
+ self,
68
+ document_id: str,
69
+ value: DocumentT,
70
+ *,
71
+ merge: bool = False,
72
+ ) -> None:
73
+ self._collection().document(document_id).set(self._to_dict(value), merge=merge)
74
+ invalidate_document_cache(self._collection_name, document_id)
75
+
76
+ def update(
77
+ self,
78
+ document_id: str,
79
+ fields: dict[str, Any],
80
+ *,
81
+ merge: bool = True,
82
+ ) -> None:
83
+ self._collection().document(document_id).set(fields, merge=merge)
84
+ invalidate_document_cache(self._collection_name, document_id)
85
+
86
+ def set_fields(
87
+ self,
88
+ document_id: str,
89
+ payload: dict[str, Any],
90
+ *,
91
+ merge: bool = True,
92
+ ) -> None:
93
+ self.update(document_id, payload, merge=merge)
94
+
95
+ def list_all(self) -> list[DocumentT]:
96
+ return [
97
+ self._from_dict(document.to_dict() or {})
98
+ for document in self._collection().stream()
99
+ ]
100
+
101
+ def document(self, document_id: str):
102
+ return self._collection().document(document_id)
103
+
104
+ def _collection(self):
105
+ return self._client_provider().collection(self._collection_name)
106
+
107
+
108
+ class SharedDocumentStore(Generic[DocumentT]):
109
+ """Typed repository for a single shared Firestore document."""
110
+
111
+ def __init__(
112
+ self,
113
+ *,
114
+ collection_name: str,
115
+ document_id: str,
116
+ from_dict: Callable[[dict[str, Any]], DocumentT],
117
+ to_dict: Callable[[DocumentT], dict[str, Any]],
118
+ client_provider: Callable[[], Any] = get_client,
119
+ cache_ttl_seconds: float | None = None,
120
+ ) -> None:
121
+ self._store = DocumentStore(
122
+ collection_name=collection_name,
123
+ from_dict=from_dict,
124
+ to_dict=to_dict,
125
+ client_provider=client_provider,
126
+ cache_ttl_seconds=cache_ttl_seconds,
127
+ )
128
+ self._document_id = document_id
129
+
130
+ def get_result(
131
+ self,
132
+ *,
133
+ force_refresh: bool = False,
134
+ ) -> FetchResult[DocumentT]:
135
+ return self._store.get_result(
136
+ self._document_id,
137
+ force_refresh=force_refresh,
138
+ )
139
+
140
+ def get(self, *, force_refresh: bool = False) -> DocumentT | None:
141
+ return self._store.get(
142
+ self._document_id,
143
+ force_refresh=force_refresh,
144
+ )
145
+
146
+ def get_or_create(self, factory: Callable[[], DocumentT]) -> DocumentT:
147
+ return self._store.get_or_create(self._document_id, factory)
148
+
149
+ def save(self, value: DocumentT, *, merge: bool = False) -> None:
150
+ self._store.save(self._document_id, value, merge=merge)
151
+
152
+ def update(self, fields: dict[str, Any], *, merge: bool = True) -> None:
153
+ self._store.update(self._document_id, fields, merge=merge)
154
+
155
+ def set_fields(self, payload: dict[str, Any], *, merge: bool = True) -> None:
156
+ self.update(payload, merge=merge)
157
+
158
+ def document(self):
159
+ return self._store.document(self._document_id)
160
+
161
+ __all__ = [
162
+ "DocumentStore",
163
+ "SharedDocumentStore",
164
+ ]
@@ -0,0 +1,228 @@
1
+ """Higher-level Firestore document fetch helpers with optional caching."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass
7
+ import time
8
+ from typing import Any, Callable, Generic, TypeVar
9
+
10
+ from telebotkit.firestore.client import FirestoreQuotaError, get_client, raise_if_quota_error
11
+
12
+ DocumentT = TypeVar("DocumentT")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class FetchResult(Generic[DocumentT]):
17
+ """Result of fetching one Firestore document."""
18
+
19
+ collection_name: str
20
+ document_id: str
21
+ found: bool
22
+ data: DocumentT | None = None
23
+ raw_data: dict[str, Any] | None = None
24
+ error: Exception | None = None
25
+ from_cache: bool = False
26
+
27
+ @property
28
+ def ok(self) -> bool:
29
+ return self.error is None
30
+
31
+ @property
32
+ def missing(self) -> bool:
33
+ return self.error is None and not self.found
34
+
35
+ @property
36
+ def has_data(self) -> bool:
37
+ return self.error is None and self.found and self.data is not None
38
+
39
+ @property
40
+ def is_quota_error(self) -> bool:
41
+ return isinstance(self.error, FirestoreQuotaError)
42
+
43
+
44
+ @dataclass
45
+ class _CacheEntry:
46
+ found: bool
47
+ raw_data: dict[str, Any] | None
48
+ expires_at: float
49
+
50
+
51
+ _DOCUMENT_CACHE: dict[tuple[str, str], _CacheEntry] = {}
52
+
53
+
54
+ def clear_document_cache() -> None:
55
+ """Drop all cached document fetch entries."""
56
+
57
+ _DOCUMENT_CACHE.clear()
58
+
59
+
60
+ def invalidate_document_cache(collection_name: str, document_id: str) -> None:
61
+ """Drop one cached document fetch entry."""
62
+
63
+ _DOCUMENT_CACHE.pop((collection_name, document_id), None)
64
+
65
+
66
+ def fetch_document(
67
+ collection_name: str,
68
+ document_id: str,
69
+ *,
70
+ parse: Callable[[dict[str, Any]], DocumentT] | None = None,
71
+ client_provider: Callable[[], Any] = get_client,
72
+ cache_ttl_seconds: float | None = None,
73
+ force_refresh: bool = False,
74
+ ) -> FetchResult[Any]:
75
+ """Fetch one Firestore document as raw data or a parsed value."""
76
+
77
+ cache_key = (collection_name, document_id)
78
+ if not force_refresh and cache_ttl_seconds and cache_ttl_seconds > 0:
79
+ cached_entry = _DOCUMENT_CACHE.get(cache_key)
80
+ if cached_entry is not None:
81
+ if cached_entry.expires_at > time.monotonic():
82
+ return _result_from_entry(
83
+ collection_name=collection_name,
84
+ document_id=document_id,
85
+ entry=cached_entry,
86
+ parse=parse,
87
+ from_cache=True,
88
+ )
89
+ invalidate_document_cache(collection_name, document_id)
90
+
91
+ try:
92
+ document = client_provider().collection(collection_name).document(document_id).get()
93
+ if not document.exists:
94
+ _cache_entry(
95
+ cache_key,
96
+ found=False,
97
+ raw_data=None,
98
+ cache_ttl_seconds=cache_ttl_seconds,
99
+ )
100
+ return FetchResult(
101
+ collection_name=collection_name,
102
+ document_id=document_id,
103
+ found=False,
104
+ )
105
+
106
+ raw_data = dict(document.to_dict() or {})
107
+ _cache_entry(
108
+ cache_key,
109
+ found=True,
110
+ raw_data=raw_data,
111
+ cache_ttl_seconds=cache_ttl_seconds,
112
+ )
113
+ return _build_result(
114
+ collection_name=collection_name,
115
+ document_id=document_id,
116
+ found=True,
117
+ raw_data=raw_data,
118
+ parse=parse,
119
+ from_cache=False,
120
+ )
121
+ except Exception as error:
122
+ normalized_error = _normalize_error(error)
123
+ return FetchResult(
124
+ collection_name=collection_name,
125
+ document_id=document_id,
126
+ found=False,
127
+ error=normalized_error,
128
+ )
129
+
130
+
131
+ def _result_from_entry(
132
+ *,
133
+ collection_name: str,
134
+ document_id: str,
135
+ entry: _CacheEntry,
136
+ parse: Callable[[dict[str, Any]], DocumentT] | None,
137
+ from_cache: bool,
138
+ ) -> FetchResult[Any]:
139
+ return _build_result(
140
+ collection_name=collection_name,
141
+ document_id=document_id,
142
+ found=entry.found,
143
+ raw_data=deepcopy(entry.raw_data),
144
+ parse=parse,
145
+ from_cache=from_cache,
146
+ )
147
+
148
+
149
+ def _build_result(
150
+ *,
151
+ collection_name: str,
152
+ document_id: str,
153
+ found: bool,
154
+ raw_data: dict[str, Any] | None,
155
+ parse: Callable[[dict[str, Any]], DocumentT] | None,
156
+ from_cache: bool,
157
+ ) -> FetchResult[Any]:
158
+ if not found:
159
+ return FetchResult(
160
+ collection_name=collection_name,
161
+ document_id=document_id,
162
+ found=False,
163
+ from_cache=from_cache,
164
+ )
165
+
166
+ safe_raw = deepcopy(raw_data) if raw_data is not None else {}
167
+ if parse is None:
168
+ return FetchResult(
169
+ collection_name=collection_name,
170
+ document_id=document_id,
171
+ found=True,
172
+ data=safe_raw,
173
+ raw_data=safe_raw,
174
+ from_cache=from_cache,
175
+ )
176
+
177
+ try:
178
+ parsed = parse(deepcopy(safe_raw))
179
+ except Exception as error:
180
+ return FetchResult(
181
+ collection_name=collection_name,
182
+ document_id=document_id,
183
+ found=True,
184
+ raw_data=safe_raw,
185
+ error=error,
186
+ from_cache=from_cache,
187
+ )
188
+
189
+ return FetchResult(
190
+ collection_name=collection_name,
191
+ document_id=document_id,
192
+ found=True,
193
+ data=parsed,
194
+ raw_data=safe_raw,
195
+ from_cache=from_cache,
196
+ )
197
+
198
+
199
+ def _cache_entry(
200
+ cache_key: tuple[str, str],
201
+ *,
202
+ found: bool,
203
+ raw_data: dict[str, Any] | None,
204
+ cache_ttl_seconds: float | None,
205
+ ) -> None:
206
+ if not cache_ttl_seconds or cache_ttl_seconds <= 0:
207
+ return
208
+ _DOCUMENT_CACHE[cache_key] = _CacheEntry(
209
+ found=found,
210
+ raw_data=deepcopy(raw_data),
211
+ expires_at=time.monotonic() + cache_ttl_seconds,
212
+ )
213
+
214
+
215
+ def _normalize_error(error: Exception) -> Exception:
216
+ try:
217
+ raise_if_quota_error(error)
218
+ except Exception as normalized_error:
219
+ return normalized_error
220
+ return error
221
+
222
+
223
+ __all__ = [
224
+ "FetchResult",
225
+ "clear_document_cache",
226
+ "fetch_document",
227
+ "invalidate_document_cache",
228
+ ]
@@ -0,0 +1,74 @@
1
+ """Firestore-backed short-lived lease helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from typing import Any
8
+
9
+ from telebotkit.firestore.client import get_client
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class LeaseStore:
15
+ """Acquire and renew short-lived Firestore document leases."""
16
+
17
+ def __init__(
18
+ self,
19
+ *,
20
+ collection_name: str,
21
+ client_provider=get_client,
22
+ ) -> None:
23
+ self._collection_name = collection_name
24
+ self._client_provider = client_provider
25
+
26
+ def acquire(
27
+ self,
28
+ *,
29
+ lease_name: str,
30
+ owner_id: str,
31
+ ttl_seconds: float,
32
+ ) -> bool:
33
+ from google.cloud import firestore # type: ignore
34
+
35
+ client = self._client_provider()
36
+ document_reference = client.collection(self._collection_name).document(lease_name)
37
+ transaction = client.transaction()
38
+ now = time.time()
39
+ expires_at = now + max(ttl_seconds, 1.0)
40
+
41
+ @firestore.transactional
42
+ def _acquire(transaction_context: Any) -> bool:
43
+ snapshot = document_reference.get(transaction=transaction_context)
44
+ if snapshot.exists:
45
+ data = snapshot.to_dict() or {}
46
+ current_owner = str(data.get("owner_id") or "")
47
+ current_expires_at = float(data.get("expires_at") or 0.0)
48
+ if current_owner and current_owner != owner_id and current_expires_at > now:
49
+ return False
50
+ transaction_context.set(
51
+ document_reference,
52
+ {
53
+ "owner_id": owner_id,
54
+ "expires_at": expires_at,
55
+ "updated_at": now,
56
+ },
57
+ )
58
+ return True
59
+
60
+ return bool(_acquire(transaction))
61
+
62
+ def try_acquire(
63
+ self,
64
+ *,
65
+ lease_name: str,
66
+ owner_id: str,
67
+ ttl_seconds: float,
68
+ ) -> bool:
69
+ return self.acquire(
70
+ lease_name=lease_name,
71
+ owner_id=owner_id,
72
+ ttl_seconds=ttl_seconds,
73
+ )
74
+ __all__ = ["LeaseStore"]
@@ -0,0 +1,75 @@
1
+ """Firestore upload helpers for prevalidated JSON payloads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from telebotkit.firestore.client import get_client
8
+ from telebotkit.sheets import load_typed_rows_json, validate_typed_rows_payload
9
+
10
+
11
+ def upload_typed_rows_payload(
12
+ *,
13
+ payload: dict[str, Any],
14
+ collection_name: str,
15
+ document_id: str,
16
+ document_type: str = "typed_rows",
17
+ records_field: str = "records",
18
+ metadata_field: str = "metadata",
19
+ key_field_name: str = "key",
20
+ ) -> str:
21
+ validated = validate_typed_rows_payload(
22
+ payload,
23
+ document_type=document_type,
24
+ records_field=records_field,
25
+ metadata_field=metadata_field,
26
+ key_field_name=key_field_name,
27
+ )
28
+ get_client().collection(collection_name).document(document_id).set(
29
+ {
30
+ "document_type": document_type,
31
+ "schema_version": int(validated.get("schema_version", 1) or 1),
32
+ records_field: validated.get(records_field) or {},
33
+ metadata_field: validated.get(metadata_field) or {},
34
+ },
35
+ merge=True,
36
+ )
37
+ meta = validated.get(metadata_field) or {}
38
+ return (
39
+ "업로드 완료:\n"
40
+ f"- Firestore: {collection_name}/{document_id}\n"
41
+ f"- record_count: {meta.get('record_count')}\n"
42
+ f"- size_bytes: {meta.get('size_bytes')}\n"
43
+ )
44
+
45
+
46
+ def upload_typed_rows_json(
47
+ *,
48
+ json_path: str,
49
+ collection_name: str,
50
+ document_id: str,
51
+ document_type: str = "typed_rows",
52
+ records_field: str = "records",
53
+ metadata_field: str = "metadata",
54
+ key_field_name: str = "key",
55
+ ) -> str:
56
+ payload = load_typed_rows_json(
57
+ json_path,
58
+ document_type=document_type,
59
+ records_field=records_field,
60
+ metadata_field=metadata_field,
61
+ key_field_name=key_field_name,
62
+ )
63
+ result = upload_typed_rows_payload(
64
+ payload=payload,
65
+ collection_name=collection_name,
66
+ document_id=document_id,
67
+ document_type=document_type,
68
+ records_field=records_field,
69
+ metadata_field=metadata_field,
70
+ key_field_name=key_field_name,
71
+ )
72
+ return result.replace("업로드 완료:\n", f"업로드 완료:\n- JSON: {json_path}\n", 1)
73
+
74
+
75
+ __all__ = ["upload_typed_rows_json", "upload_typed_rows_payload"]