lanternfish 0.3.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.
@@ -0,0 +1,61 @@
1
+ # __init__.py
2
+ # Görev: Lanternfish SDK'nin public API'sini dışa aktarmak
3
+ # Yapmaz: Instrumentation mantığı içermez
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ from lanternfish.context import UserScope, set_user
8
+ from lanternfish.decorators import span, span_context, trace
9
+ from lanternfish.deployments import record_deployment
10
+ from lanternfish.errors import LanternfishConfigurationError, LanternfishDeploymentError
11
+ from lanternfish.instrument import flush, instrument, shutdown
12
+ from lanternfish.semantic import (
13
+ confidence_tracking,
14
+ context_retrieval,
15
+ decision,
16
+ memory_access,
17
+ planning,
18
+ reasoning,
19
+ reflection,
20
+ set_planning_attribute,
21
+ set_reasoning_attribute,
22
+ set_reflection_attribute,
23
+ set_verification_attribute,
24
+ tool_usage,
25
+ verification,
26
+ )
27
+
28
+ # Sürüm tek kaynaktan, pyproject'ten okunur; kaynak ağaçtan çalışırken
29
+ # paket kurulu olmayabilir.
30
+ try:
31
+ __version__ = version("lanternfish")
32
+ except PackageNotFoundError:
33
+ __version__ = "0.0.0+local"
34
+
35
+ __all__ = [
36
+ "__version__",
37
+ "instrument",
38
+ "flush",
39
+ "shutdown",
40
+ "UserScope",
41
+ "LanternfishConfigurationError",
42
+ "LanternfishDeploymentError",
43
+ "record_deployment",
44
+ "set_user",
45
+ "trace",
46
+ "span",
47
+ "span_context",
48
+ "planning",
49
+ "reasoning",
50
+ "reflection",
51
+ "verification",
52
+ "tool_usage",
53
+ "memory_access",
54
+ "context_retrieval",
55
+ "confidence_tracking",
56
+ "decision",
57
+ "set_planning_attribute",
58
+ "set_reasoning_attribute",
59
+ "set_reflection_attribute",
60
+ "set_verification_attribute",
61
+ ]
@@ -0,0 +1,81 @@
1
+ """
2
+ SDK yapılandırmasının çözümü.
3
+
4
+ Görev: Endpoint, API anahtarı, sürüm (release) ve ortam değerlerini argümandan ya da ortam değişkeninden okur ve
5
+ doğrular. Sürüm verilmezse yaygın CI/hosting değişkenlerinden (Git commit) bulur.
6
+ Yapmaz: Ağa çıkmaz, provider kurmaz, varsayılan bir Cloud adresi seçmez.
7
+ """
8
+
9
+ import os
10
+ from urllib.parse import urlsplit
11
+
12
+ from lanternfish.errors import LanternfishConfigurationError
13
+
14
+ # Sürümü kendiliğinden bulmak için okunan değişkenler; ilk dolu olan kazanır. Platformların kendi adları.
15
+ RELEASE_ENV_VARS = (
16
+ "LANTERNFISH_RELEASE",
17
+ "GIT_COMMIT_SHA",
18
+ "GITHUB_SHA",
19
+ "VERCEL_GIT_COMMIT_SHA",
20
+ "RENDER_GIT_COMMIT",
21
+ "RAILWAY_GIT_COMMIT_SHA",
22
+ "HEROKU_SLUG_COMMIT",
23
+ "SOURCE_COMMIT",
24
+ )
25
+ SHORT_SHA_LENGTH = 12
26
+
27
+
28
+ def resolve_endpoint(endpoint: str | None) -> str:
29
+ """API taban adresini argümandan veya LANTERNFISH_ENDPOINT'ten çözer ve doğrular.
30
+
31
+ Raises:
32
+ LanternfishConfigurationError: Adres yoksa, mutlak http(s) adresi değilse veya sorgu içeriyorsa.
33
+ """
34
+ value = (endpoint or os.getenv("LANTERNFISH_ENDPOINT", "")).strip().rstrip("/")
35
+ if not value:
36
+ raise LanternfishConfigurationError(
37
+ "Lanternfish endpoint is required. Pass endpoint=... or set LANTERNFISH_ENDPOINT. "
38
+ "The SDK never selects a destination automatically."
39
+ )
40
+
41
+ parsed = urlsplit(value)
42
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
43
+ raise LanternfishConfigurationError("Lanternfish endpoint must be an absolute http:// or https:// URL.")
44
+ if parsed.query or parsed.fragment:
45
+ raise LanternfishConfigurationError("Lanternfish endpoint must not contain a query string or fragment.")
46
+ return value
47
+
48
+
49
+ def resolve_api_key(api_key: str | None) -> str:
50
+ """API anahtarını argümandan veya LANTERNFISH_API_KEY'den çözer.
51
+
52
+ Raises:
53
+ LanternfishConfigurationError: Anahtar yoksa.
54
+ """
55
+ value = (api_key or os.getenv("LANTERNFISH_API_KEY", "")).strip()
56
+ if not value:
57
+ raise LanternfishConfigurationError(
58
+ "Lanternfish API key is required. Pass api_key=... or set LANTERNFISH_API_KEY. "
59
+ "Create one in Settings → API keys."
60
+ )
61
+ return value
62
+
63
+
64
+ def resolve_release(release: str | None) -> str | None:
65
+ """Çalışan kodun sürümünü döndürür: argüman, LANTERNFISH_RELEASE ya da CI'ın commit değişkeni.
66
+
67
+ Commit SHA'sı ilk 12 karaktere kısaltılır; sürüm adı (ör. "v1.8.2") olduğu gibi kalır.
68
+ """
69
+ if release and release.strip():
70
+ return release.strip()
71
+ for name in RELEASE_ENV_VARS:
72
+ value = os.getenv(name, "").strip()
73
+ if value:
74
+ return value if name == "LANTERNFISH_RELEASE" else value[:SHORT_SHA_LENGTH]
75
+ return None
76
+
77
+
78
+ def resolve_environment(environment: str | None) -> str | None:
79
+ """Ortam adını (ör. "production") argümandan veya LANTERNFISH_ENVIRONMENT'tan döndürür."""
80
+ value = (environment or os.getenv("LANTERNFISH_ENVIRONMENT", "")).strip()
81
+ return value or None
lanternfish/context.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Son kullanıcı kimliği.
3
+
4
+ Görev: Agent'ı kullanan son kullanıcının kimliğini, oturumunu ve etiketlerini bağlama koyar; bundan sonra açılan her
5
+ span bu bilgiyi taşır. `with` ile kullanılırsa blok bitince eski bağlama döner.
6
+ Yapmaz: Span açmaz veya göndermez; değeri span'e damgalamak span_processor.py'nin işidir.
7
+ """
8
+
9
+ from types import TracebackType
10
+
11
+ from opentelemetry import context
12
+
13
+ from lanternfish.span_processor import SESSION_ID_KEY, TAGS_KEY, USER_ID_KEY
14
+
15
+
16
+ class UserScope:
17
+ """`set_user()` sonucunun kapsamı: `with` bloğundan çıkınca kimliği bağlamdan kaldırır."""
18
+
19
+ def __init__(self, token: object) -> None:
20
+ """Kapsamı, bağlama eklenen değerin geri alma anahtarıyla kurar."""
21
+ self._token = token
22
+ self._closed = False
23
+
24
+ def __enter__(self) -> "UserScope":
25
+ """Kimlik zaten bağlamda; bloğa olduğu gibi girer."""
26
+ return self
27
+
28
+ def __exit__(
29
+ self,
30
+ exc_type: type[BaseException] | None,
31
+ exc: BaseException | None,
32
+ traceback: TracebackType | None,
33
+ ) -> None:
34
+ """Bloktan çıkarken kimliği bağlamdan kaldırır."""
35
+ self.close()
36
+
37
+ def close(self) -> None:
38
+ """Kimliği bağlamdan kaldırır; ikinci çağrı etkisizdir."""
39
+ if not self._closed:
40
+ context.detach(self._token) # type: ignore[arg-type]
41
+ self._closed = True
42
+
43
+
44
+ def set_user(user_id: str, session_id: str | None = None, tags: dict[str, str] | None = None) -> UserScope:
45
+ """Bundan sonra açılan span'lere son kullanıcı kimliğini ekler.
46
+
47
+ Web sunucusunda istek başına `with` ile kullanın; böylece kimlik bir sonraki isteğe taşınmaz::
48
+
49
+ with lanternfish.set_user("user_42", session_id="sess_9"):
50
+ run_agent(question)
51
+
52
+ Args:
53
+ user_id: Son kullanıcının kimliği.
54
+ session_id: Sohbet veya oturum kimliği.
55
+ tags: Run'ı süzmek için düz etiketler.
56
+
57
+ Returns:
58
+ `with` ile kullanılabilen ya da `close()` ile kapatılabilen kapsam.
59
+ """
60
+ ctx = context.get_current()
61
+ if user_id:
62
+ ctx = context.set_value(USER_ID_KEY, user_id, ctx)
63
+ if session_id:
64
+ ctx = context.set_value(SESSION_ID_KEY, session_id, ctx)
65
+ if tags:
66
+ ctx = context.set_value(TAGS_KEY, tags, ctx)
67
+ return UserScope(context.attach(ctx))
@@ -0,0 +1,222 @@
1
+ # decorators.py
2
+ # Gorev: @trace ve @span decorator + span() context manager ile manual instrumentation
3
+ # Yapmaz: TracerProvider veya exporter olusturmaz; instrument() ile aktif provider kullanilir
4
+ #
5
+ # Kullanim:
6
+ # @trace -> span adi = fn adi, parametreler otomatik yakalanir
7
+ # @trace(name="x") -> custom span adi
8
+ # @span -> span adi = fn adi, parametreler yakalanmaz
9
+ # @span("x") / @span(name="x") -> custom span adi, decorator
10
+ # with span("x") as s: -> context manager, manuel attribute
11
+ # with span_context("x") as s: -> span_context alias'i
12
+
13
+ import functools
14
+ import inspect
15
+ from contextlib import contextmanager
16
+ from typing import Any, Callable, Generator
17
+
18
+ from opentelemetry import trace as otel_trace
19
+ from opentelemetry.trace import StatusCode
20
+
21
+
22
+ # ── @trace ────────────────────────────────────────────────────────────────────
23
+
24
+ def trace(func: Callable | None = None, *, name: str | None = None, capture_args: bool = True) -> Any:
25
+ """Custom fonksiyonlari OpenTelemetry span olarak izler.
26
+
27
+ Parametreler otomatik olarak manual.param.<isim> attribute'i olur.
28
+ Exception durumunda span status ERROR olarak isaretlenir.
29
+
30
+ Iki kullanim sekli:
31
+ @trace -> span adi fonksiyon adindan alinir
32
+ @trace(name="custom_name") -> span adi manuel verilir
33
+
34
+ Args:
35
+ func: Direkt decorator olarak kullanildiginda gelen fonksiyon.
36
+ name: Opsiyonel custom span adi.
37
+ capture_args: False ise parametre degerleri span'e yazilmaz (kisisel veri tasiyan fonksiyonlar icin).
38
+ """
39
+ def decorator(fn: Callable) -> Callable:
40
+ span_name = name or fn.__name__
41
+
42
+ if inspect.iscoroutinefunction(fn):
43
+ @functools.wraps(fn)
44
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
45
+ tracer = otel_trace.get_tracer("lanternfish.manual")
46
+ with tracer.start_as_current_span(span_name) as s:
47
+ if capture_args:
48
+ _add_params(s, fn, args, kwargs)
49
+ try:
50
+ result = await fn(*args, **kwargs)
51
+ s.set_status(StatusCode.OK)
52
+ return result
53
+ except Exception as exc:
54
+ s.set_status(StatusCode.ERROR, str(exc))
55
+ s.record_exception(exc)
56
+ raise
57
+ return async_wrapper
58
+ else:
59
+ @functools.wraps(fn)
60
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
61
+ tracer = otel_trace.get_tracer("lanternfish.manual")
62
+ with tracer.start_as_current_span(span_name) as s:
63
+ if capture_args:
64
+ _add_params(s, fn, args, kwargs)
65
+ try:
66
+ result = fn(*args, **kwargs)
67
+ s.set_status(StatusCode.OK)
68
+ return result
69
+ except Exception as exc:
70
+ s.set_status(StatusCode.ERROR, str(exc))
71
+ s.record_exception(exc)
72
+ raise
73
+ return sync_wrapper
74
+
75
+ if func is None:
76
+ return decorator
77
+ return decorator(func)
78
+
79
+
80
+ # ── @span / span() context manager ────────────────────────────────────────────
81
+
82
+ class _SpanProxy:
83
+ """span("name") cagrisi icin dual-use proxy.
84
+
85
+ Hem @span("name") decorator hem with span("name") as s: context manager
86
+ olarak kullanilabilir. Parametreler otomatik yakalanmaz; caller s.set_attribute()
87
+ ile manuel ekler.
88
+ """
89
+
90
+ def __init__(self, span_name: str) -> None:
91
+ self._span_name = span_name
92
+ self._cm: Any = None
93
+
94
+ def __call__(self, fn: Callable) -> Callable:
95
+ """@span("name") seklinde decorator olarak kullanildiginda."""
96
+ return _make_span_wrapper(fn, self._span_name)
97
+
98
+ def __enter__(self) -> Any:
99
+ """with span("name") as s: seklinde context manager olarak kullanildiginda."""
100
+ tracer = otel_trace.get_tracer("lanternfish.manual")
101
+ self._cm = tracer.start_as_current_span(self._span_name)
102
+ s = self._cm.__enter__()
103
+ self._span_ref = s
104
+ return s
105
+
106
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
107
+ if exc_val is not None:
108
+ self._span_ref.set_status(StatusCode.ERROR, str(exc_val))
109
+ self._span_ref.record_exception(exc_val)
110
+ else:
111
+ self._span_ref.set_status(StatusCode.OK)
112
+ return self._cm.__exit__(exc_type, exc_val, exc_tb)
113
+
114
+ # async context manager destegi
115
+ async def __aenter__(self) -> Any:
116
+ return self.__enter__()
117
+
118
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
119
+ return self.__exit__(exc_type, exc_val, exc_tb)
120
+
121
+
122
+ def span(func_or_name: Callable | str | None = None, *, name: str | None = None) -> Any:
123
+ """Kod blogunu veya fonksiyonu izler. Hafif versiyon (@trace'in aksine parametre yakalamaz).
124
+
125
+ Uc kullanim sekli:
126
+ @span -> decorator, span adi = fn adi
127
+ @span("custom") / @span(name=..) -> decorator, custom span adi
128
+ with span("name") as s: -> context manager, s.set_attribute() ile manuel attr
129
+
130
+ Args:
131
+ func_or_name: Fonksiyon (@span direkt), string (span adi) veya None.
132
+ name: Keyword-only custom span adi.
133
+ """
134
+ # Durum 1: @span (parametresiz, direkt fonksiyon uzerinde)
135
+ if callable(func_or_name):
136
+ return _make_span_wrapper(func_or_name, func_or_name.__name__)
137
+
138
+ # Durum 2: span("name") veya span(name="name") -> _SpanProxy doner
139
+ actual_name: str = (
140
+ func_or_name if isinstance(func_or_name, str)
141
+ else name or "span"
142
+ )
143
+ return _SpanProxy(actual_name)
144
+
145
+
146
+ @contextmanager
147
+ def span_context(name: str) -> Generator[Any, None, None]:
148
+ """Context manager olarak span olusturur.
149
+
150
+ Kullanim:
151
+ with span_context("operation") as s:
152
+ s.set_attribute("key", "value")
153
+
154
+ Args:
155
+ name: Span adi.
156
+
157
+ Yields:
158
+ Aktif OpenTelemetry span.
159
+ """
160
+ tracer = otel_trace.get_tracer("lanternfish.manual")
161
+ with tracer.start_as_current_span(name) as s:
162
+ try:
163
+ yield s
164
+ s.set_status(StatusCode.OK)
165
+ except Exception as exc:
166
+ s.set_status(StatusCode.ERROR, str(exc))
167
+ s.record_exception(exc)
168
+ raise
169
+
170
+
171
+ # ── Internal helpers ──────────────────────────────────────────────────────────
172
+
173
+ def _make_span_wrapper(fn: Callable, span_name: str) -> Callable:
174
+ """Sync veya async fonksiyon icin span wrapper olusturur (parametresiz)."""
175
+ if inspect.iscoroutinefunction(fn):
176
+ @functools.wraps(fn)
177
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
178
+ tracer = otel_trace.get_tracer("lanternfish.manual")
179
+ with tracer.start_as_current_span(span_name) as s:
180
+ try:
181
+ result = await fn(*args, **kwargs)
182
+ s.set_status(StatusCode.OK)
183
+ return result
184
+ except Exception as exc:
185
+ s.set_status(StatusCode.ERROR, str(exc))
186
+ s.record_exception(exc)
187
+ raise
188
+ return async_wrapper
189
+ else:
190
+ @functools.wraps(fn)
191
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
192
+ tracer = otel_trace.get_tracer("lanternfish.manual")
193
+ with tracer.start_as_current_span(span_name) as s:
194
+ try:
195
+ result = fn(*args, **kwargs)
196
+ s.set_status(StatusCode.OK)
197
+ return result
198
+ except Exception as exc:
199
+ s.set_status(StatusCode.ERROR, str(exc))
200
+ s.record_exception(exc)
201
+ raise
202
+ return sync_wrapper
203
+
204
+
205
+ def _add_params(s: Any, fn: Callable, args: tuple, kwargs: dict) -> None:
206
+ """Fonksiyon parametrelerini span attribute olarak ekler (trace icin).
207
+
208
+ self ve cls atlanir. 500 karakteri asan degerler kisaltilir.
209
+ """
210
+ try:
211
+ sig = inspect.signature(fn)
212
+ bound = sig.bind(*args, **kwargs)
213
+ bound.apply_defaults()
214
+ for param_name, param_value in bound.arguments.items():
215
+ if param_name in ("self", "cls"):
216
+ continue
217
+ value_str = str(param_value)
218
+ if len(value_str) > 500:
219
+ value_str = value_str[:500] + "... [truncated]"
220
+ s.set_attribute(f"manual.param.{param_name}", value_str)
221
+ except Exception:
222
+ pass
@@ -0,0 +1,69 @@
1
+ """Deployment kaydı — sürümü Lanternfish'e bildirir.
2
+
3
+ Görev: Bir sürümün ne zaman yayına çıktığını Lanternfish'e kaydeder; Lanternfish bu kaydı olay listesinde gösterir ve
4
+ "deployment öncesi bir saat" ile karşılaştırma yapar. CI betiğinden `instrument()` çağrılmadan kullanılır.
5
+ Yapmaz: Span üretmez, tekrar denemez; başarısızlığı sessizce yutmaz (CI adımı hatayı görmeli).
6
+ """
7
+
8
+ from datetime import datetime
9
+
10
+ import requests
11
+
12
+ from lanternfish.configuration import resolve_api_key, resolve_endpoint
13
+ from lanternfish.errors import LanternfishDeploymentError
14
+
15
+ MetadataValue = str | int | float | bool
16
+
17
+
18
+ def record_deployment(
19
+ version: str,
20
+ *,
21
+ deployed_at: datetime | None = None,
22
+ metadata: dict[str, MetadataValue] | None = None,
23
+ api_key: str | None = None,
24
+ endpoint: str | None = None,
25
+ timeout_seconds: int = 10,
26
+ ) -> str:
27
+ """Bir deployment kaydeder ve Lanternfish'in verdiği kimliği döndürür.
28
+
29
+ Örnek (CI'da, deploy adımından sonra)::
30
+
31
+ import lanternfish
32
+ lanternfish.record_deployment("v1.4.2", metadata={"commit": "a1b2c3d", "environment": "production"})
33
+
34
+ Args:
35
+ version: Yayına çıkan sürüm (ör. "v1.4.2" veya commit kısaltması).
36
+ deployed_at: Deployment'ın bittiği zaman; verilmezse Lanternfish isteği aldığı anı kullanır. Saat dilimi verin.
37
+ metadata: En çok 20 düz etiket (ör. commit, ortam).
38
+ api_key: Proje API anahtarı; verilmezse LANTERNFISH_API_KEY.
39
+ endpoint: Lanternfish API adresi; verilmezse LANTERNFISH_ENDPOINT.
40
+ timeout_seconds: İstek için üst süre.
41
+
42
+ Returns:
43
+ Kaydın kimliği (ör. "dep_…").
44
+
45
+ Raises:
46
+ LanternfishConfigurationError: API anahtarı veya adres yoksa.
47
+ LanternfishDeploymentError: Lanternfish kaydı kabul etmezse veya ulaşılamazsa.
48
+ """
49
+ key = resolve_api_key(api_key)
50
+ payload: dict[str, object] = {"version": version, "metadata": metadata or {}}
51
+ if deployed_at is not None:
52
+ payload["deployed_at"] = deployed_at.isoformat()
53
+ url = f"{resolve_endpoint(endpoint)}/v1/deployments"
54
+ try:
55
+ response = requests.post(url, json=payload, headers={"x-api-key": key}, timeout=timeout_seconds)
56
+ except requests.RequestException as error:
57
+ raise LanternfishDeploymentError(f"Lanternfish could not be reached at {url}: {error}") from error
58
+ if response.status_code != 201:
59
+ raise LanternfishDeploymentError(_failure_message(response))
60
+ return str(response.json()["id"])
61
+
62
+
63
+ def _failure_message(response: requests.Response) -> str:
64
+ """Backend'in hata ayrıntısını, yoksa durum kodunu cümleye çevirir."""
65
+ try:
66
+ detail = response.json().get("detail")
67
+ except ValueError:
68
+ detail = None
69
+ return f"Lanternfish rejected the deployment ({response.status_code}): {detail or response.reason}"
lanternfish/errors.py ADDED
@@ -0,0 +1,9 @@
1
+ """Public exceptions raised by the Lanternfish SDK."""
2
+
3
+
4
+ class LanternfishConfigurationError(ValueError):
5
+ """The SDK cannot start because required configuration is missing or invalid."""
6
+
7
+
8
+ class LanternfishDeploymentError(RuntimeError):
9
+ """Lanternfish did not record the deployment (rejected, or unreachable)."""
@@ -0,0 +1,34 @@
1
+ """
2
+ Gönderim zinciri.
3
+
4
+ Görev: Damgalayıcıyı ve toplu JSON göndericisini tracer provider'a bağlar.
5
+ Yapmaz: TracerProvider oluşturmaz, enstrümantasyon yapmaz.
6
+ """
7
+
8
+ from opentelemetry.sdk.trace import TracerProvider
9
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
10
+
11
+ from lanternfish.json_exporter import JSONSpanExporter
12
+ from lanternfish.span_processor import LanternfishContextProcessor
13
+
14
+
15
+ def configure_exporter(
16
+ tracer_provider: TracerProvider,
17
+ api_key: str,
18
+ endpoint: str,
19
+ static_attributes: dict[str, str] | None = None,
20
+ ) -> None:
21
+ """Damgalayıcıyı ve toplu göndericiyi tracer provider'a bağlar.
22
+
23
+ Toplu gönderim arka planda yapılır; agent her span için ağı beklemez. Bekleyen span'ler süreç kapanırken veya
24
+ `lanternfish.flush()` çağrılınca gönderilir.
25
+
26
+ Args:
27
+ tracer_provider: İşlemcilerin ekleneceği provider.
28
+ api_key: Proje API anahtarı.
29
+ endpoint: Backend taban adresi.
30
+ static_attributes: Her span'e yazılacak sabit öznitelikler (agent adı, sürüm, ortam).
31
+ """
32
+ # Damgalayıcı önce eklenir: span başlarken çalışması gerekir.
33
+ tracer_provider.add_span_processor(LanternfishContextProcessor(static_attributes))
34
+ tracer_provider.add_span_processor(BatchSpanProcessor(JSONSpanExporter(endpoint=endpoint, api_key=api_key)))