laplace-python-sdk 1.1.1__tar.gz → 1.2.1__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.
Files changed (29) hide show
  1. {laplace_python_sdk-1.1.1/src/laplace_python_sdk.egg-info → laplace_python_sdk-1.2.1}/PKG-INFO +1 -1
  2. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/pyproject.toml +6 -4
  3. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/models.py +19 -0
  4. laplace_python_sdk-1.2.1/src/laplace/news.py +333 -0
  5. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1/src/laplace_python_sdk.egg-info}/PKG-INFO +1 -1
  6. laplace_python_sdk-1.1.1/src/laplace/news.py +0 -94
  7. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/LICENSE +0 -0
  8. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/MANIFEST.in +0 -0
  9. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/README.md +0 -0
  10. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/setup.cfg +0 -0
  11. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/__init__.py +0 -0
  12. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/base.py +0 -0
  13. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/brokers.py +0 -0
  14. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/capital_increase.py +0 -0
  15. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/client.py +0 -0
  16. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/collections.py +0 -0
  17. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/earnings.py +0 -0
  18. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/financials.py +0 -0
  19. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/funds.py +0 -0
  20. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/live_price.py +0 -0
  21. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/politician.py +0 -0
  22. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/search.py +0 -0
  23. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/state.py +0 -0
  24. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/stocks.py +0 -0
  25. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace/websocket.py +0 -0
  26. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace_python_sdk.egg-info/SOURCES.txt +0 -0
  27. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace_python_sdk.egg-info/dependency_links.txt +0 -0
  28. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace_python_sdk.egg-info/requires.txt +0 -0
  29. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.1}/src/laplace_python_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laplace-python-sdk
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: Python SDK for Laplace stock data platform
5
5
  Author: Laplace SDK Team
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "laplace-python-sdk"
7
- version = "1.1.1"
7
+ version = "1.2.1"
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.1.1"
67
+ python_version = "1.2.1"
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.1.1"
74
+ target-version = "1.2.1"
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"]
@@ -908,6 +908,25 @@ class News(BaseModel):
908
908
 
909
909
  quality_score: int = Field(alias="qualityScore")
910
910
 
911
+ model_config = {"populate_by_name": True}
912
+
913
+ class NewsV2(BaseModel):
914
+ created_at: datetime = Field(alias="createdAt")
915
+ url: str
916
+ image_url: str = Field(alias="imageUrl")
917
+ timestamp: datetime
918
+ publisher_url: str = Field(alias="publisherUrl")
919
+
920
+ publisher: NewsPublisher
921
+
922
+ tickers: Optional[List[NewsTicker]] = None
923
+ categories: Optional[NewsCategory] = None
924
+ sectors: Optional[NewsSector] = None
925
+ content: Optional[NewsContent] = None
926
+ industries: Optional[NewsIndustry] = None
927
+
928
+ quality_score: int = Field(alias="qualityScore")
929
+
911
930
  model_config = {"populate_by_name": True}
912
931
  class NewsHighlight(BaseModel):
913
932
  consumer: List[str]
@@ -0,0 +1,333 @@
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
+ NewsV2,
14
+ NewsHighlight,
15
+ NewsOrderBy,
16
+ NewsType,
17
+ PaginatedResponse,
18
+ PaginationPageSize,
19
+ Region,
20
+ SortDirection,
21
+ T,
22
+ )
23
+
24
+
25
+ class NewsStreamResult(Generic[T]):
26
+ """Result wrapper for news stream data."""
27
+
28
+ def __init__(self, data: Optional[T] = None, error: Optional[str] = None):
29
+ self.data = data
30
+ self.error = error
31
+
32
+ @property
33
+ def is_error(self) -> bool:
34
+ return self.error is not None
35
+
36
+
37
+ class NewsStream:
38
+ """Handles Server-Sent Events (SSE) stream for news."""
39
+
40
+ def __init__(
41
+ self,
42
+ base_client: BaseClient,
43
+ locale: Locale,
44
+ region: Region,
45
+ sectors: Optional[List[str]] = None,
46
+ tickers: Optional[List[str]] = None,
47
+ categories: Optional[List[str]] = None,
48
+ industries: Optional[List[str]] = None,
49
+ ):
50
+ self.base_client = base_client
51
+ self.locale = locale
52
+ self.region = region
53
+ self.sectors = sectors
54
+ self.tickers = tickers
55
+ self.categories = categories
56
+ self.industries = industries
57
+ self._task: Optional[asyncio.Task] = None
58
+ self._queue: Optional[asyncio.Queue[NewsStreamResult[List[NewsV2]]]] = None
59
+ self._is_closed = False
60
+
61
+ async def subscribe(self) -> None:
62
+ """Subscribe to news updates stream."""
63
+ await self._cleanup_existing_stream()
64
+
65
+ self._queue = asyncio.Queue[NewsStreamResult[List[NewsV2]]]()
66
+ self._is_closed = False
67
+ self._task = asyncio.create_task(self._start_streaming())
68
+
69
+ async def receive(self) -> AsyncGenerator[NewsStreamResult[List[NewsV2]], None]:
70
+ """Receive news data from the stream."""
71
+ if not self._queue:
72
+ raise RuntimeError("Not subscribed. Call subscribe() first.")
73
+
74
+ while not self._is_closed:
75
+ try:
76
+ result = await asyncio.wait_for(self._queue.get(), timeout=1.0)
77
+ yield result
78
+ except asyncio.TimeoutError:
79
+ continue
80
+ except asyncio.CancelledError:
81
+ break
82
+
83
+ async def close(self) -> None:
84
+ """Close the stream and cleanup resources."""
85
+ if self._is_closed:
86
+ return
87
+
88
+ self._is_closed = True
89
+ await self._cleanup_existing_stream()
90
+
91
+ async def _cleanup_existing_stream(self) -> None:
92
+ """Cancel and cleanup existing streaming task."""
93
+ if self._task and not self._task.done():
94
+ self._task.cancel()
95
+ try:
96
+ await self._task
97
+ except asyncio.CancelledError:
98
+ pass
99
+
100
+ def _build_stream_url(self) -> str:
101
+ """Build the streaming URL for the news endpoint."""
102
+ url = f"{self.base_client.base_url}/v1/news/stream"
103
+ params = {"locale": self.locale, "region": self.region.value}
104
+ if self.sectors:
105
+ params["sectors"] = ",".join(self.sectors)
106
+ if self.tickers:
107
+ params["tickers"] = ",".join(self.tickers)
108
+ if self.categories:
109
+ params["categories"] = ",".join(self.categories)
110
+ if self.industries:
111
+ params["industries"] = ",".join(self.industries)
112
+
113
+ query_string = urllib.parse.urlencode(params)
114
+ return f"{url}?{query_string}"
115
+
116
+ async def _start_streaming(self) -> None:
117
+ """Start the SSE streaming connection."""
118
+ url = self._build_stream_url()
119
+ headers = {
120
+ "Accept": "text/event-stream",
121
+ "Cache-Control": "no-cache",
122
+ "Connection": "keep-alive",
123
+ "Authorization": f"Bearer {self.base_client.api_key}",
124
+ }
125
+
126
+ try:
127
+ async with httpx.AsyncClient(timeout=30.0) as client:
128
+ async with client.stream("GET", url, headers=headers) as response:
129
+ if response.status_code != 200:
130
+ error_body = await response.aread()
131
+ error_msg = f"News stream failed: {response.status_code} - "
132
+ error_msg += f"{error_body.decode()}"
133
+ await self._put_error(error_msg)
134
+ return
135
+
136
+ await self._process_stream_lines(response)
137
+
138
+ except (httpx.TimeoutException, httpx.ConnectError) as e:
139
+ await self._put_error(f"Connection error: {e}")
140
+ except Exception as e:
141
+ await self._put_error(f"Streaming error: {e}")
142
+ finally:
143
+ self._is_closed = True
144
+
145
+ async def _process_stream_lines(self, response) -> None:
146
+ """Process individual lines from the SSE stream."""
147
+ async for line in response.aiter_lines():
148
+ if self._is_closed:
149
+ break
150
+
151
+ if not line.startswith("data:"):
152
+ continue
153
+
154
+ try:
155
+ # Parse the JSON data after "data:" prefix
156
+ json_data = line[5:].strip() # Remove "data:" prefix
157
+ if not json_data:
158
+ continue
159
+
160
+ parsed_data = json.loads(json_data)
161
+
162
+ # Process array of news items
163
+ news_items = [NewsV2(**item) for item in parsed_data]
164
+ result = NewsStreamResult[List[NewsV2]](data=news_items)
165
+ await self._queue.put(result)
166
+
167
+ except Exception as e:
168
+ await self._put_error(f"Error processing news data: {e}")
169
+ continue
170
+
171
+ async def _put_error(self, error_message: str) -> None:
172
+ """Put an error result in the queue."""
173
+ if self._queue:
174
+ error_result = NewsStreamResult[List[NewsV2]](error=error_message)
175
+ await self._queue.put(error_result)
176
+
177
+
178
+ class NewsClient:
179
+ """Client for news API endpoints."""
180
+
181
+ def __init__(self, base_client: BaseClient):
182
+ """Initialize the news client.
183
+
184
+ Args:
185
+ base_client: The base Laplace client instance
186
+ """
187
+ self._client = base_client
188
+
189
+ def get_news(
190
+ self,
191
+ locale: Locale,
192
+ region: Region,
193
+ news_type: Optional[NewsType] = None,
194
+ news_order_by: Optional[NewsOrderBy] = None,
195
+ direction: Optional[SortDirection] = None,
196
+ extra_filters: Optional[str] = None,
197
+ page: int = 0,
198
+ page_size: PaginationPageSize = PaginationPageSize.PAGE_SIZE_10,
199
+ ) -> PaginatedResponse[News]:
200
+ """Retrieve paginated news.
201
+
202
+ Args:
203
+ locale: Locale code (e.g. "tr", "en")
204
+ region: Region enum (e.g. Region.TR)
205
+ news_type: Optional news type filter
206
+ news_order_by: Optional sorting field
207
+ direction: Optional sort direction
208
+ extra_filters: Optional extra filters (API-specific)
209
+ page: Page number (default: 0)
210
+ page_size: Page size enum (default: 10)
211
+
212
+ Returns:
213
+ PaginatedResponse[News]
214
+ """
215
+ params: Dict[str, object] = {
216
+ "locale": locale,
217
+ "region": region.value,
218
+ "page": page,
219
+ "size": page_size.value,
220
+ }
221
+
222
+ if news_type is not None:
223
+ params["newsType"] = news_type.value
224
+ if news_order_by is not None:
225
+ params["orderBy"] = news_order_by.value
226
+ if direction is not None:
227
+ params["orderByDirection"] = direction.value
228
+ if extra_filters:
229
+ params["extraFilters"] = extra_filters
230
+
231
+ response = self._client.get("v1/news", params=params)
232
+ return PaginatedResponse[News](**response)
233
+
234
+ def get_news_v2(
235
+ self,
236
+ locale: Locale,
237
+ region: Region,
238
+ news_type: Optional[NewsType] = None,
239
+ news_order_by: Optional[NewsOrderBy] = None,
240
+ direction: Optional[SortDirection] = None,
241
+ extra_filters: Optional[str] = None,
242
+ page: int = 0,
243
+ page_size: PaginationPageSize = PaginationPageSize.PAGE_SIZE_10,
244
+ ) -> PaginatedResponse[NewsV2]:
245
+ """Retrieve paginated news (v2).
246
+
247
+ Args:
248
+ locale: Locale code (e.g. "tr", "en")
249
+ region: Region enum (e.g. Region.TR)
250
+ news_type: Optional news type filter
251
+ news_order_by: Optional sorting field
252
+ direction: Optional sort direction
253
+ extra_filters: Optional extra filters (API-specific)
254
+ page: Page number (default: 0)
255
+ page_size: Page size enum (default: 10)
256
+
257
+ Returns:
258
+ PaginatedResponse[NewsV2]
259
+ """
260
+ params: Dict[str, object] = {
261
+ "locale": locale,
262
+ "region": region.value,
263
+ "page": page,
264
+ "size": page_size.value,
265
+ }
266
+
267
+ if news_type is not None:
268
+ params["newsType"] = news_type.value
269
+ if news_order_by is not None:
270
+ params["orderBy"] = news_order_by.value
271
+ if direction is not None:
272
+ params["orderByDirection"] = direction.value
273
+ if extra_filters:
274
+ params["extraFilters"] = extra_filters
275
+
276
+ response = self._client.get("v2/news", params=params)
277
+ return PaginatedResponse[NewsV2](**response)
278
+
279
+ def get_highlights(
280
+ self,
281
+ locale: Locale,
282
+ region: Region
283
+ ) -> NewsHighlight:
284
+ """Retrieve news highlights.
285
+
286
+ Args:
287
+ locale: Locale code (e.g. "tr", "en")
288
+ region: Region enum (e.g. Region.TR)
289
+
290
+ Returns:
291
+ NewsHighlight
292
+ """
293
+ params: Dict[str, object] = {
294
+ "locale": locale,
295
+ "region": region.value
296
+ }
297
+
298
+ response = self._client.get("v1/news/highlights", params=params)
299
+ return NewsHighlight(**response)
300
+
301
+ async def get_news_stream(
302
+ self,
303
+ locale: Locale,
304
+ region: Region,
305
+ sectors: Optional[List[str]] = None,
306
+ tickers: Optional[List[str]] = None,
307
+ categories: Optional[List[str]] = None,
308
+ industries: Optional[List[str]] = None,
309
+ ) -> NewsStream:
310
+ """Start streaming news updates.
311
+
312
+ Args:
313
+ locale: Locale code (e.g., "tr", "en")
314
+ region: Region enum (e.g. Region.TR)
315
+ sectors: Optional list of sectors
316
+ tickers: Optional list of tickers
317
+ categories: Optional list of categories
318
+ industries: Optional list of industries
319
+
320
+ Returns:
321
+ NewsStream for consuming news items
322
+ """
323
+ stream = NewsStream(
324
+ self._client,
325
+ locale,
326
+ region,
327
+ sectors=sectors,
328
+ tickers=tickers,
329
+ categories=categories,
330
+ industries=industries,
331
+ )
332
+ await stream.subscribe()
333
+ return stream
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laplace-python-sdk
3
- Version: 1.1.1
3
+ Version: 1.2.1
4
4
  Summary: Python SDK for Laplace stock data platform
5
5
  Author: Laplace SDK Team
6
6
  License: MIT
@@ -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)