laplace-python-sdk 1.1.1__tar.gz → 1.2.0__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.
- {laplace_python_sdk-1.1.1/src/laplace_python_sdk.egg-info → laplace_python_sdk-1.2.0}/PKG-INFO +1 -1
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/pyproject.toml +6 -4
- laplace_python_sdk-1.2.0/src/laplace/news.py +282 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0/src/laplace_python_sdk.egg-info}/PKG-INFO +1 -1
- laplace_python_sdk-1.1.1/src/laplace/news.py +0 -94
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/LICENSE +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/MANIFEST.in +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/README.md +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/setup.cfg +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/__init__.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/base.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/brokers.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/capital_increase.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/client.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/collections.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/earnings.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/financials.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/funds.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/live_price.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/models.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/politician.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/search.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/state.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/stocks.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/websocket.py +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/SOURCES.txt +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/dependency_links.txt +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/requires.txt +0 -0
- {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "laplace-python-sdk"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Python SDK for Laplace stock data platform"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -64,16 +64,18 @@ profile = "black"
|
|
|
64
64
|
line_length = 100
|
|
65
65
|
|
|
66
66
|
[tool.mypy]
|
|
67
|
-
python_version = "1.
|
|
67
|
+
python_version = "1.2.0"
|
|
68
68
|
strict = true
|
|
69
69
|
warn_return_any = true
|
|
70
70
|
warn_unused_configs = true
|
|
71
71
|
|
|
72
72
|
[tool.ruff]
|
|
73
73
|
line-length = 100
|
|
74
|
-
target-version = "1.
|
|
74
|
+
target-version = "1.2.0"
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
75
77
|
select = ["E", "W", "F", "I", "N", "B", "UP"]
|
|
76
78
|
ignore = ["B008", "N805"]
|
|
77
79
|
|
|
78
|
-
[tool.ruff.isort]
|
|
80
|
+
[tool.ruff.lint.isort]
|
|
79
81
|
known-first-party = ["laplace"]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import urllib.parse
|
|
4
|
+
from typing import AsyncGenerator, Dict, Generic, List, Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from laplace.base import BaseClient
|
|
9
|
+
|
|
10
|
+
from .models import (
|
|
11
|
+
Locale,
|
|
12
|
+
News,
|
|
13
|
+
NewsHighlight,
|
|
14
|
+
NewsOrderBy,
|
|
15
|
+
NewsType,
|
|
16
|
+
PaginatedResponse,
|
|
17
|
+
PaginationPageSize,
|
|
18
|
+
Region,
|
|
19
|
+
SortDirection,
|
|
20
|
+
T,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NewsStreamResult(Generic[T]):
|
|
25
|
+
"""Result wrapper for news stream data."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, data: Optional[T] = None, error: Optional[str] = None):
|
|
28
|
+
self.data = data
|
|
29
|
+
self.error = error
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_error(self) -> bool:
|
|
33
|
+
return self.error is not None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NewsStream:
|
|
37
|
+
"""Handles Server-Sent Events (SSE) stream for news."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
base_client: BaseClient,
|
|
42
|
+
locale: Locale,
|
|
43
|
+
sectors: Optional[List[str]] = None,
|
|
44
|
+
tickers: Optional[List[str]] = None,
|
|
45
|
+
categories: Optional[List[str]] = None,
|
|
46
|
+
industries: Optional[List[str]] = None,
|
|
47
|
+
):
|
|
48
|
+
self.base_client = base_client
|
|
49
|
+
self.locale = locale
|
|
50
|
+
self.sectors = sectors
|
|
51
|
+
self.tickers = tickers
|
|
52
|
+
self.categories = categories
|
|
53
|
+
self.industries = industries
|
|
54
|
+
self._task: Optional[asyncio.Task] = None
|
|
55
|
+
self._queue: Optional[asyncio.Queue[NewsStreamResult[List[News]]]] = None
|
|
56
|
+
self._is_closed = False
|
|
57
|
+
|
|
58
|
+
async def subscribe(self) -> None:
|
|
59
|
+
"""Subscribe to news updates stream."""
|
|
60
|
+
await self._cleanup_existing_stream()
|
|
61
|
+
|
|
62
|
+
self._queue = asyncio.Queue[NewsStreamResult[List[News]]]()
|
|
63
|
+
self._is_closed = False
|
|
64
|
+
self._task = asyncio.create_task(self._start_streaming())
|
|
65
|
+
|
|
66
|
+
async def receive(self) -> AsyncGenerator[NewsStreamResult[List[News]], None]:
|
|
67
|
+
"""Receive news data from the stream."""
|
|
68
|
+
if not self._queue:
|
|
69
|
+
raise RuntimeError("Not subscribed. Call subscribe() first.")
|
|
70
|
+
|
|
71
|
+
while not self._is_closed:
|
|
72
|
+
try:
|
|
73
|
+
result = await asyncio.wait_for(self._queue.get(), timeout=1.0)
|
|
74
|
+
yield result
|
|
75
|
+
except asyncio.TimeoutError:
|
|
76
|
+
continue
|
|
77
|
+
except asyncio.CancelledError:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
async def close(self) -> None:
|
|
81
|
+
"""Close the stream and cleanup resources."""
|
|
82
|
+
if self._is_closed:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
self._is_closed = True
|
|
86
|
+
await self._cleanup_existing_stream()
|
|
87
|
+
|
|
88
|
+
async def _cleanup_existing_stream(self) -> None:
|
|
89
|
+
"""Cancel and cleanup existing streaming task."""
|
|
90
|
+
if self._task and not self._task.done():
|
|
91
|
+
self._task.cancel()
|
|
92
|
+
try:
|
|
93
|
+
await self._task
|
|
94
|
+
except asyncio.CancelledError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
def _build_stream_url(self) -> str:
|
|
98
|
+
"""Build the streaming URL for the news endpoint."""
|
|
99
|
+
url = f"{self.base_client.base_url}/v1/news/stream"
|
|
100
|
+
params = {"locale": self.locale}
|
|
101
|
+
if self.sectors:
|
|
102
|
+
params["sectors"] = ",".join(self.sectors)
|
|
103
|
+
if self.tickers:
|
|
104
|
+
params["tickers"] = ",".join(self.tickers)
|
|
105
|
+
if self.categories:
|
|
106
|
+
params["categories"] = ",".join(self.categories)
|
|
107
|
+
if self.industries:
|
|
108
|
+
params["industries"] = ",".join(self.industries)
|
|
109
|
+
|
|
110
|
+
query_string = urllib.parse.urlencode(params)
|
|
111
|
+
return f"{url}?{query_string}"
|
|
112
|
+
|
|
113
|
+
async def _start_streaming(self) -> None:
|
|
114
|
+
"""Start the SSE streaming connection."""
|
|
115
|
+
url = self._build_stream_url()
|
|
116
|
+
headers = {
|
|
117
|
+
"Accept": "text/event-stream",
|
|
118
|
+
"Cache-Control": "no-cache",
|
|
119
|
+
"Connection": "keep-alive",
|
|
120
|
+
"Authorization": f"Bearer {self.base_client.api_key}",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
125
|
+
async with client.stream("GET", url, headers=headers) as response:
|
|
126
|
+
if response.status_code != 200:
|
|
127
|
+
error_body = await response.aread()
|
|
128
|
+
error_msg = f"News stream failed: {response.status_code} - "
|
|
129
|
+
error_msg += f"{error_body.decode()}"
|
|
130
|
+
await self._put_error(error_msg)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
await self._process_stream_lines(response)
|
|
134
|
+
|
|
135
|
+
except (httpx.TimeoutException, httpx.ConnectError) as e:
|
|
136
|
+
await self._put_error(f"Connection error: {e}")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
await self._put_error(f"Streaming error: {e}")
|
|
139
|
+
finally:
|
|
140
|
+
self._is_closed = True
|
|
141
|
+
|
|
142
|
+
async def _process_stream_lines(self, response) -> None:
|
|
143
|
+
"""Process individual lines from the SSE stream."""
|
|
144
|
+
async for line in response.aiter_lines():
|
|
145
|
+
if self._is_closed:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
if not line.startswith("data:"):
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
# Parse the JSON data after "data:" prefix
|
|
153
|
+
json_data = line[5:].strip() # Remove "data:" prefix
|
|
154
|
+
if not json_data:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
parsed_data = json.loads(json_data)
|
|
158
|
+
|
|
159
|
+
# Process array of news items
|
|
160
|
+
news_items = [News(**item) for item in parsed_data]
|
|
161
|
+
result = NewsStreamResult[List[News]](data=news_items)
|
|
162
|
+
await self._queue.put(result)
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
await self._put_error(f"Error processing news data: {e}")
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
async def _put_error(self, error_message: str) -> None:
|
|
169
|
+
"""Put an error result in the queue."""
|
|
170
|
+
if self._queue:
|
|
171
|
+
error_result = NewsStreamResult[List[News]](error=error_message)
|
|
172
|
+
await self._queue.put(error_result)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class NewsClient:
|
|
176
|
+
"""Client for news API endpoints."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, base_client: BaseClient):
|
|
179
|
+
"""Initialize the news client.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
base_client: The base Laplace client instance
|
|
183
|
+
"""
|
|
184
|
+
self._client = base_client
|
|
185
|
+
|
|
186
|
+
def get_news(
|
|
187
|
+
self,
|
|
188
|
+
locale: Locale,
|
|
189
|
+
region: Region,
|
|
190
|
+
news_type: Optional[NewsType] = None,
|
|
191
|
+
news_order_by: Optional[NewsOrderBy] = None,
|
|
192
|
+
direction: Optional[SortDirection] = None,
|
|
193
|
+
extra_filters: Optional[str] = None,
|
|
194
|
+
page: int = 0,
|
|
195
|
+
page_size: PaginationPageSize = PaginationPageSize.PAGE_SIZE_10,
|
|
196
|
+
) -> PaginatedResponse[News]:
|
|
197
|
+
"""Retrieve paginated news.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
locale: Locale code (e.g. "tr", "en")
|
|
201
|
+
region: Region enum (e.g. Region.TR)
|
|
202
|
+
news_type: Optional news type filter
|
|
203
|
+
news_order_by: Optional sorting field
|
|
204
|
+
direction: Optional sort direction
|
|
205
|
+
extra_filters: Optional extra filters (API-specific)
|
|
206
|
+
page: Page number (default: 0)
|
|
207
|
+
page_size: Page size enum (default: 10)
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
PaginatedResponse[News]
|
|
211
|
+
"""
|
|
212
|
+
params: Dict[str, object] = {
|
|
213
|
+
"locale": locale,
|
|
214
|
+
"region": region.value,
|
|
215
|
+
"page": page,
|
|
216
|
+
"size": page_size.value,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if news_type is not None:
|
|
220
|
+
params["newsType"] = news_type.value
|
|
221
|
+
if news_order_by is not None:
|
|
222
|
+
params["orderBy"] = news_order_by.value
|
|
223
|
+
if direction is not None:
|
|
224
|
+
params["orderByDirection"] = direction.value
|
|
225
|
+
if extra_filters:
|
|
226
|
+
params["extraFilters"] = extra_filters
|
|
227
|
+
|
|
228
|
+
response = self._client.get("v1/news", params=params)
|
|
229
|
+
return PaginatedResponse[News](**response)
|
|
230
|
+
|
|
231
|
+
def get_highlights(
|
|
232
|
+
self,
|
|
233
|
+
locale: Locale,
|
|
234
|
+
region: Region
|
|
235
|
+
) -> NewsHighlight:
|
|
236
|
+
"""Retrieve news highlights.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
locale: Locale code (e.g. "tr", "en")
|
|
240
|
+
region: Region enum (e.g. Region.TR)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
NewsHighlight
|
|
244
|
+
"""
|
|
245
|
+
params: Dict[str, object] = {
|
|
246
|
+
"locale": locale,
|
|
247
|
+
"region": region.value
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
response = self._client.get("v1/news/highlights", params=params)
|
|
251
|
+
return NewsHighlight(**response)
|
|
252
|
+
|
|
253
|
+
async def get_news_stream(
|
|
254
|
+
self,
|
|
255
|
+
locale: Locale,
|
|
256
|
+
sectors: Optional[List[str]] = None,
|
|
257
|
+
tickers: Optional[List[str]] = None,
|
|
258
|
+
categories: Optional[List[str]] = None,
|
|
259
|
+
industries: Optional[List[str]] = None,
|
|
260
|
+
) -> NewsStream:
|
|
261
|
+
"""Start streaming news updates.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
locale: Locale code (e.g., "tr", "en")
|
|
265
|
+
sectors: Optional list of sectors
|
|
266
|
+
tickers: Optional list of tickers
|
|
267
|
+
categories: Optional list of categories
|
|
268
|
+
industries: Optional list of industries
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
NewsStream for consuming news items
|
|
272
|
+
"""
|
|
273
|
+
stream = NewsStream(
|
|
274
|
+
self._client,
|
|
275
|
+
locale,
|
|
276
|
+
sectors=sectors,
|
|
277
|
+
tickers=tickers,
|
|
278
|
+
categories=categories,
|
|
279
|
+
industries=industries,
|
|
280
|
+
)
|
|
281
|
+
await stream.subscribe()
|
|
282
|
+
return stream
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
from typing import Dict, Optional
|
|
2
|
-
|
|
3
|
-
from laplace.base import BaseClient
|
|
4
|
-
|
|
5
|
-
from .models import (
|
|
6
|
-
Locale,
|
|
7
|
-
News,
|
|
8
|
-
NewsHighlight,
|
|
9
|
-
NewsOrderBy,
|
|
10
|
-
NewsType,
|
|
11
|
-
PaginatedResponse,
|
|
12
|
-
Region,
|
|
13
|
-
PaginationPageSize,
|
|
14
|
-
SortDirection,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class NewsClient:
|
|
19
|
-
"""Client for news API endpoints."""
|
|
20
|
-
|
|
21
|
-
def __init__(self, base_client: BaseClient):
|
|
22
|
-
"""Initialize the news client.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
base_client: The base Laplace client instance
|
|
26
|
-
"""
|
|
27
|
-
self._client = base_client
|
|
28
|
-
|
|
29
|
-
def get_news(
|
|
30
|
-
self,
|
|
31
|
-
locale: Locale,
|
|
32
|
-
region: Region,
|
|
33
|
-
news_type: Optional[NewsType] = None,
|
|
34
|
-
news_order_by: Optional[NewsOrderBy] = None,
|
|
35
|
-
direction: Optional[SortDirection] = None,
|
|
36
|
-
extra_filters: Optional[str] = None,
|
|
37
|
-
page: int = 0,
|
|
38
|
-
page_size: PaginationPageSize = PaginationPageSize.PAGE_SIZE_10,
|
|
39
|
-
) -> PaginatedResponse[News]:
|
|
40
|
-
"""Retrieve paginated news.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
locale: Locale code (e.g. "tr", "en")
|
|
44
|
-
region: Region enum (e.g. Region.TR)
|
|
45
|
-
news_type: Optional news type filter
|
|
46
|
-
news_order_by: Optional sorting field
|
|
47
|
-
direction: Optional sort direction
|
|
48
|
-
extra_filters: Optional extra filters (API-specific)
|
|
49
|
-
page: Page number (default: 0)
|
|
50
|
-
page_size: Page size enum (default: 10)
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
PaginatedResponse[News]
|
|
54
|
-
"""
|
|
55
|
-
params: Dict[str, object] = {
|
|
56
|
-
"locale": locale,
|
|
57
|
-
"region": region.value,
|
|
58
|
-
"page": page,
|
|
59
|
-
"size": page_size.value,
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if news_type is not None:
|
|
63
|
-
params["newsType"] = news_type.value
|
|
64
|
-
if news_order_by is not None:
|
|
65
|
-
params["orderBy"] = news_order_by.value
|
|
66
|
-
if direction is not None:
|
|
67
|
-
params["orderByDirection"] = direction.value
|
|
68
|
-
if extra_filters:
|
|
69
|
-
params["extraFilters"] = extra_filters
|
|
70
|
-
|
|
71
|
-
response = self._client.get("v1/news", params=params)
|
|
72
|
-
return PaginatedResponse[News](**response)
|
|
73
|
-
|
|
74
|
-
def get_highlights(
|
|
75
|
-
self,
|
|
76
|
-
locale: Locale,
|
|
77
|
-
region: Region
|
|
78
|
-
) -> NewsHighlight:
|
|
79
|
-
"""Retrieve news highlights.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
locale: Locale code (e.g. "tr", "en")
|
|
83
|
-
region: Region enum (e.g. Region.TR)
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
NewsHighlight
|
|
87
|
-
"""
|
|
88
|
-
params: Dict[str, object] = {
|
|
89
|
-
"locale": locale,
|
|
90
|
-
"region": region.value
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
response = self._client.get("v1/news/highlights", params=params)
|
|
94
|
-
return NewsHighlight(**response)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/requires.txt
RENAMED
|
File without changes
|
{laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/top_level.txt
RENAMED
|
File without changes
|