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.
Files changed (29) hide show
  1. {laplace_python_sdk-1.1.1/src/laplace_python_sdk.egg-info → laplace_python_sdk-1.2.0}/PKG-INFO +1 -1
  2. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/pyproject.toml +6 -4
  3. laplace_python_sdk-1.2.0/src/laplace/news.py +282 -0
  4. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0/src/laplace_python_sdk.egg-info}/PKG-INFO +1 -1
  5. laplace_python_sdk-1.1.1/src/laplace/news.py +0 -94
  6. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/LICENSE +0 -0
  7. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/MANIFEST.in +0 -0
  8. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/README.md +0 -0
  9. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/setup.cfg +0 -0
  10. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/__init__.py +0 -0
  11. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/base.py +0 -0
  12. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/brokers.py +0 -0
  13. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/capital_increase.py +0 -0
  14. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/client.py +0 -0
  15. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/collections.py +0 -0
  16. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/earnings.py +0 -0
  17. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/financials.py +0 -0
  18. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/funds.py +0 -0
  19. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/live_price.py +0 -0
  20. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/models.py +0 -0
  21. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/politician.py +0 -0
  22. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/search.py +0 -0
  23. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/state.py +0 -0
  24. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/stocks.py +0 -0
  25. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace/websocket.py +0 -0
  26. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/SOURCES.txt +0 -0
  27. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/dependency_links.txt +0 -0
  28. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/src/laplace_python_sdk.egg-info/requires.txt +0 -0
  29. {laplace_python_sdk-1.1.1 → laplace_python_sdk-1.2.0}/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.0
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.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.1.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.1.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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: laplace-python-sdk
3
- Version: 1.1.1
3
+ Version: 1.2.0
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)