finbrain-python 0.2.3__py3-none-any.whl → 0.2.5__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.
finbrain/aio/client.py CHANGED
@@ -24,10 +24,12 @@ from .endpoints.screener import AsyncScreenerAPI
24
24
  from .endpoints.recent import AsyncRecentAPI
25
25
  from .endpoints.corporate_lobbying import AsyncCorporateLobbyingAPI
26
26
  from .endpoints.reddit_mentions import AsyncRedditMentionsAPI
27
+ from .endpoints.government_contracts import AsyncGovernmentContractsAPI
28
+ from .endpoints.patent_filings import AsyncPatentFilingsAPI
27
29
 
28
30
 
29
- # Which status codes merit a retry
30
- _RETRYABLE_STATUS = {500}
31
+ # Which status codes merit a retry (transient server / gateway errors)
32
+ _RETRYABLE_STATUS = {500, 502, 503, 504}
31
33
  # How long to wait between retries (2, 4, 8 … seconds)
32
34
  _BACKOFF_BASE = 2
33
35
 
@@ -84,6 +86,8 @@ class AsyncFinBrainClient:
84
86
  self.recent = AsyncRecentAPI(self)
85
87
  self.corporate_lobbying = AsyncCorporateLobbyingAPI(self)
86
88
  self.reddit_mentions = AsyncRedditMentionsAPI(self)
89
+ self.government_contracts = AsyncGovernmentContractsAPI(self)
90
+ self.patent_filings = AsyncPatentFilingsAPI(self)
87
91
 
88
92
  async def __aenter__(self) -> "AsyncFinBrainClient":
89
93
  """Context manager entry."""
@@ -162,7 +166,7 @@ class AsyncFinBrainClient:
162
166
 
163
167
  # ── Error path ───────────────────────────────────
164
168
  if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
165
- # 500 – exponential back-off then retry
169
+ # Transient server/gateway error – exponential back-off then retry
166
170
  await asyncio.sleep(_BACKOFF_BASE**attempt)
167
171
  continue
168
172
 
@@ -1,37 +1,12 @@
1
1
  """
2
2
  Shared utility functions for async endpoint modules.
3
3
 
4
- This module contains common helper functions used across multiple async endpoint
5
- implementations to avoid code duplication.
4
+ These helpers are identical to the synchronous ones, so they are re-exported
5
+ from :mod:`finbrain.endpoints._utils` to keep a single source of truth.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
- import datetime as _dt
10
9
 
10
+ from ...endpoints._utils import to_datestr
11
11
 
12
- def to_datestr(value: _dt.date | str) -> str:
13
- """
14
- Convert datetime.date to ISO format string (YYYY-MM-DD).
15
-
16
- If the input is already a string, it is returned unchanged.
17
- This allows flexible date parameter handling in API calls.
18
-
19
- Parameters
20
- ----------
21
- value : datetime.date or str
22
- Date value to convert.
23
-
24
- Returns
25
- -------
26
- str
27
- ISO format date string (YYYY-MM-DD).
28
-
29
- Examples
30
- --------
31
- >>> from datetime import date
32
- >>> to_datestr(date(2025, 1, 15))
33
- '2025-01-15'
34
- >>> to_datestr("2025-01-15")
35
- '2025-01-15'
36
- """
37
- return value.isoformat() if isinstance(value, _dt.date) else value
12
+ __all__ = ["to_datestr"]
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from typing import TYPE_CHECKING, Dict, Any, List
5
+
6
+ from ._utils import to_datestr
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import AsyncFinBrainClient
10
+
11
+
12
+ class AsyncGovernmentContractsAPI:
13
+ """Async wrapper for /government-contracts and /screener/government-contracts endpoints."""
14
+
15
+ def __init__(self, client: "AsyncFinBrainClient") -> None:
16
+ self._c = client
17
+
18
+ async def ticker(
19
+ self,
20
+ symbol: str,
21
+ *,
22
+ date_from: _dt.date | str | None = None,
23
+ date_to: _dt.date | str | None = None,
24
+ limit: int | None = None,
25
+ as_dataframe: bool = False,
26
+ ) -> Dict[str, Any] | pd.DataFrame:
27
+ """Fetch government contract awards for a symbol (async)."""
28
+ params: Dict[str, str] = {}
29
+ if date_from:
30
+ params["startDate"] = to_datestr(date_from)
31
+ if date_to:
32
+ params["endDate"] = to_datestr(date_to)
33
+ if limit is not None:
34
+ params["limit"] = str(limit)
35
+
36
+ path = f"government-contracts/{symbol.upper()}"
37
+
38
+ data: Dict[str, Any] = await self._c._request("GET", path, params=params)
39
+
40
+ if as_dataframe:
41
+ rows: List[Dict[str, Any]] = data.get("contracts", [])
42
+ df = pd.DataFrame(rows)
43
+ if not df.empty and "startDate" in df.columns:
44
+ df["startDate"] = pd.to_datetime(df["startDate"])
45
+ df.set_index("startDate", inplace=True)
46
+ return df
47
+
48
+ return data
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from typing import TYPE_CHECKING, Dict, Any, List
5
+
6
+ from ._utils import to_datestr
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import AsyncFinBrainClient
10
+
11
+
12
+ class AsyncPatentFilingsAPI:
13
+ """Async wrapper for /patent-filings and /screener/patent-filings endpoints."""
14
+
15
+ def __init__(self, client: "AsyncFinBrainClient") -> None:
16
+ self._c = client
17
+
18
+ async def ticker(
19
+ self,
20
+ symbol: str,
21
+ *,
22
+ date_from: _dt.date | str | None = None,
23
+ date_to: _dt.date | str | None = None,
24
+ limit: int | None = None,
25
+ as_dataframe: bool = False,
26
+ ) -> Dict[str, Any] | pd.DataFrame:
27
+ """Fetch USPTO granted patents for a symbol (async)."""
28
+ params: Dict[str, str] = {}
29
+ if date_from:
30
+ params["startDate"] = to_datestr(date_from)
31
+ if date_to:
32
+ params["endDate"] = to_datestr(date_to)
33
+ if limit is not None:
34
+ params["limit"] = str(limit)
35
+
36
+ path = f"patent-filings/{symbol.upper()}"
37
+
38
+ data: Dict[str, Any] = await self._c._request("GET", path, params=params)
39
+
40
+ if as_dataframe:
41
+ rows: List[Dict[str, Any]] = data.get("patents", [])
42
+ df = pd.DataFrame(rows)
43
+ if not df.empty and "patentDate" in df.columns:
44
+ df["patentDate"] = pd.to_datetime(df["patentDate"])
45
+ df.set_index("patentDate", inplace=True)
46
+ return df
47
+
48
+ return data
@@ -222,3 +222,27 @@ class AsyncScreenerAPI:
222
222
  """Screen Reddit mention counts across tickers (async)."""
223
223
  params = self._build_params(limit=limit, market=market, region=region)
224
224
  return await self._get("screener/reddit-mentions", params, as_dataframe)
225
+
226
+ # ── government contracts ──────────────────────────────────
227
+
228
+ async def government_contracts(
229
+ self,
230
+ *,
231
+ limit: int | None = None,
232
+ as_dataframe: bool = False,
233
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
234
+ """Screen government contracts across all tickers (async)."""
235
+ params = self._build_params(limit=limit)
236
+ return await self._get("screener/government-contracts", params, as_dataframe)
237
+
238
+ # ── patent filings ────────────────────────────────────────
239
+
240
+ async def patent_filings(
241
+ self,
242
+ *,
243
+ limit: int | None = None,
244
+ as_dataframe: bool = False,
245
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
246
+ """Screen USPTO patent filings across all tickers (async)."""
247
+ params = self._build_params(limit=limit)
248
+ return await self._get("screener/patent-filings", params, as_dataframe)
finbrain/client.py CHANGED
@@ -25,10 +25,12 @@ from .endpoints.screener import ScreenerAPI
25
25
  from .endpoints.recent import RecentAPI
26
26
  from .endpoints.corporate_lobbying import CorporateLobbyingAPI
27
27
  from .endpoints.reddit_mentions import RedditMentionsAPI
28
+ from .endpoints.government_contracts import GovernmentContractsAPI
29
+ from .endpoints.patent_filings import PatentFilingsAPI
28
30
 
29
31
 
30
- # Which status codes merit a retry
31
- _RETRYABLE_STATUS = {500}
32
+ # Which status codes merit a retry (transient server / gateway errors)
33
+ _RETRYABLE_STATUS = {500, 502, 503, 504}
32
34
  # How long to wait between retries (2, 4, 8 … seconds)
33
35
  _BACKOFF_BASE = 2
34
36
 
@@ -79,6 +81,21 @@ class FinBrainClient:
79
81
  self.recent = RecentAPI(self)
80
82
  self.corporate_lobbying = CorporateLobbyingAPI(self)
81
83
  self.reddit_mentions = RedditMentionsAPI(self)
84
+ self.government_contracts = GovernmentContractsAPI(self)
85
+ self.patent_filings = PatentFilingsAPI(self)
86
+
87
+ # ---------- lifecycle ----------
88
+ def __enter__(self) -> "FinBrainClient":
89
+ """Context manager entry."""
90
+ return self
91
+
92
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
93
+ """Context manager exit — close the underlying HTTP session."""
94
+ self.close()
95
+
96
+ def close(self) -> None:
97
+ """Close the underlying ``requests.Session``."""
98
+ self.session.close()
82
99
 
83
100
  # ---------- private helpers ----------
84
101
  def _request(
@@ -129,7 +146,7 @@ class FinBrainClient:
129
146
 
130
147
  # ── Error path ───────────────────────────────────
131
148
  if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
132
- # 500 – exponential back-off then retry
149
+ # Transient server/gateway error – exponential back-off then retry
133
150
  time.sleep(_BACKOFF_BASE**attempt)
134
151
  continue
135
152
 
@@ -28,10 +28,16 @@ def to_datestr(value: _dt.date | str) -> str:
28
28
 
29
29
  Examples
30
30
  --------
31
- >>> from datetime import date
31
+ >>> from datetime import date, datetime
32
32
  >>> to_datestr(date(2025, 1, 15))
33
33
  '2025-01-15'
34
+ >>> to_datestr(datetime(2025, 1, 15, 9, 30))
35
+ '2025-01-15'
34
36
  >>> to_datestr("2025-01-15")
35
37
  '2025-01-15'
36
38
  """
39
+ # ``datetime`` subclasses ``date``; take the date part so the time
40
+ # component never leaks into the ``YYYY-MM-DD`` API parameter.
41
+ if isinstance(value, _dt.datetime):
42
+ return value.date().isoformat()
37
43
  return value.isoformat() if isinstance(value, _dt.date) else value
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from typing import TYPE_CHECKING, Dict, Any, List
5
+
6
+ from ._utils import to_datestr
7
+
8
+ if TYPE_CHECKING: # imported only by type-checkers
9
+ from ..client import FinBrainClient
10
+
11
+
12
+ class GovernmentContractsAPI:
13
+ """
14
+ Endpoints
15
+ ---------
16
+ ``/government-contracts/<TICKER>`` - U.S. government contract awards.
17
+ ``/screener/government-contracts`` - cross-ticker government contracts screener.
18
+ """
19
+
20
+ # ------------------------------------------------------------------ #
21
+ def __init__(self, client: "FinBrainClient") -> None:
22
+ self._c = client # reference to the parent client
23
+
24
+ # ------------------------------------------------------------------ #
25
+ def ticker(
26
+ self,
27
+ symbol: str,
28
+ *,
29
+ date_from: _dt.date | str | None = None,
30
+ date_to: _dt.date | str | None = None,
31
+ limit: int | None = None,
32
+ as_dataframe: bool = False,
33
+ ) -> Dict[str, Any] | pd.DataFrame:
34
+ """
35
+ Fetch government contract awards for *symbol*.
36
+
37
+ Parameters
38
+ ----------
39
+ symbol :
40
+ Ticker symbol; auto-upper-cased.
41
+ date_from, date_to :
42
+ Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows.
43
+ limit :
44
+ Maximum number of records to return (1-500).
45
+ as_dataframe :
46
+ If *True*, return a **pandas.DataFrame** indexed by ``startDate``;
47
+ otherwise return the raw JSON dict.
48
+
49
+ Returns
50
+ -------
51
+ dict | pandas.DataFrame
52
+ """
53
+ params: Dict[str, str] = {}
54
+ if date_from:
55
+ params["startDate"] = to_datestr(date_from)
56
+ if date_to:
57
+ params["endDate"] = to_datestr(date_to)
58
+ if limit is not None:
59
+ params["limit"] = str(limit)
60
+
61
+ path = f"government-contracts/{symbol.upper()}"
62
+
63
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
64
+
65
+ if as_dataframe:
66
+ rows: List[Dict[str, Any]] = data.get("contracts", [])
67
+ df = pd.DataFrame(rows)
68
+ if not df.empty and "startDate" in df.columns:
69
+ df["startDate"] = pd.to_datetime(df["startDate"])
70
+ df.set_index("startDate", inplace=True)
71
+ return df
72
+
73
+ return data
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from typing import TYPE_CHECKING, Dict, Any, List
5
+
6
+ from ._utils import to_datestr
7
+
8
+ if TYPE_CHECKING: # imported only by type-checkers
9
+ from ..client import FinBrainClient
10
+
11
+
12
+ class PatentFilingsAPI:
13
+ """
14
+ Endpoints
15
+ ---------
16
+ ``/patent-filings/<TICKER>`` - USPTO granted patents mapped to a ticker.
17
+ ``/screener/patent-filings`` - cross-ticker patent filings screener.
18
+ """
19
+
20
+ # ------------------------------------------------------------------ #
21
+ def __init__(self, client: "FinBrainClient") -> None:
22
+ self._c = client # reference to the parent client
23
+
24
+ # ------------------------------------------------------------------ #
25
+ def ticker(
26
+ self,
27
+ symbol: str,
28
+ *,
29
+ date_from: _dt.date | str | None = None,
30
+ date_to: _dt.date | str | None = None,
31
+ limit: int | None = None,
32
+ as_dataframe: bool = False,
33
+ ) -> Dict[str, Any] | pd.DataFrame:
34
+ """
35
+ Fetch USPTO granted patents for *symbol*.
36
+
37
+ Parameters
38
+ ----------
39
+ symbol :
40
+ Ticker symbol; auto-upper-cased.
41
+ date_from, date_to :
42
+ Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows by
43
+ grant date (``patentDate``).
44
+ limit :
45
+ Maximum number of records to return (1-500).
46
+ as_dataframe :
47
+ If *True*, return a **pandas.DataFrame** indexed by ``patentDate``;
48
+ otherwise return the raw JSON dict.
49
+
50
+ Returns
51
+ -------
52
+ dict | pandas.DataFrame
53
+ """
54
+ params: Dict[str, str] = {}
55
+ if date_from:
56
+ params["startDate"] = to_datestr(date_from)
57
+ if date_to:
58
+ params["endDate"] = to_datestr(date_to)
59
+ if limit is not None:
60
+ params["limit"] = str(limit)
61
+
62
+ path = f"patent-filings/{symbol.upper()}"
63
+
64
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
65
+
66
+ if as_dataframe:
67
+ rows: List[Dict[str, Any]] = data.get("patents", [])
68
+ df = pd.DataFrame(rows)
69
+ if not df.empty and "patentDate" in df.columns:
70
+ df["patentDate"] = pd.to_datetime(df["patentDate"])
71
+ df.set_index("patentDate", inplace=True)
72
+ return df
73
+
74
+ return data
@@ -227,3 +227,27 @@ class ScreenerAPI:
227
227
  """Screen Reddit mention counts across tickers."""
228
228
  params = self._build_params(limit=limit, market=market, region=region)
229
229
  return self._get("screener/reddit-mentions", params, as_dataframe)
230
+
231
+ # ── government contracts ──────────────────────────────────
232
+
233
+ def government_contracts(
234
+ self,
235
+ *,
236
+ limit: int | None = None,
237
+ as_dataframe: bool = False,
238
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
239
+ """Screen government contracts across all tickers."""
240
+ params = self._build_params(limit=limit)
241
+ return self._get("screener/government-contracts", params, as_dataframe)
242
+
243
+ # ── patent filings ────────────────────────────────────────
244
+
245
+ def patent_filings(
246
+ self,
247
+ *,
248
+ limit: int | None = None,
249
+ as_dataframe: bool = False,
250
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
251
+ """Screen USPTO patent filings across all tickers."""
252
+ params = self._build_params(limit=limit)
253
+ return self._get("screener/patent-filings", params, as_dataframe)
finbrain/exceptions.py CHANGED
@@ -14,6 +14,9 @@ Docs-based mapping
14
14
  405 Method Not Allowed → MethodNotAllowed
15
15
  429 Rate Limit Exceeded → RateLimitError
16
16
  500 Internal Server Error → ServerError
17
+ 502 Bad Gateway → BadGateway
18
+ 503 Service Unavailable → ServiceUnavailable
19
+ 504 Gateway Timeout → GatewayTimeout
17
20
  """
18
21
 
19
22
  from __future__ import annotations
@@ -29,6 +32,9 @@ __all__ = [
29
32
  "MethodNotAllowed",
30
33
  "RateLimitError",
31
34
  "ServerError",
35
+ "BadGateway",
36
+ "ServiceUnavailable",
37
+ "GatewayTimeout",
32
38
  #
33
39
  "InvalidResponse",
34
40
  "http_error_to_exception",
@@ -96,6 +102,18 @@ class ServerError(FinBrainError):
96
102
  """500 - Internal error on FinBrain's side. Retrying later may help."""
97
103
 
98
104
 
105
+ class BadGateway(FinBrainError):
106
+ """502 - Invalid response from an upstream server. Usually transient."""
107
+
108
+
109
+ class ServiceUnavailable(FinBrainError):
110
+ """503 - Service temporarily unavailable (overloaded or in maintenance)."""
111
+
112
+
113
+ class GatewayTimeout(FinBrainError):
114
+ """504 - Upstream server did not respond in time. Usually transient."""
115
+
116
+
99
117
  # ─────────────────────────────────────────────────────────────
100
118
  # Transport / decoding guard
101
119
  # ─────────────────────────────────────────────────────────────
@@ -167,6 +185,12 @@ def http_error_to_exception(resp) -> FinBrainError: # expects requests.Response
167
185
  return RateLimitError(message, **kwargs)
168
186
  if status == 500:
169
187
  return ServerError(message, **kwargs)
188
+ if status == 502:
189
+ return BadGateway(message, **kwargs)
190
+ if status == 503:
191
+ return ServiceUnavailable(message, **kwargs)
192
+ if status == 504:
193
+ return GatewayTimeout(message, **kwargs)
170
194
 
171
195
  # Fallback for undocumented codes (future-proofing)
172
196
  return FinBrainError(message, **kwargs)