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 +7 -3
- finbrain/aio/endpoints/_utils.py +4 -29
- finbrain/aio/endpoints/government_contracts.py +48 -0
- finbrain/aio/endpoints/patent_filings.py +48 -0
- finbrain/aio/endpoints/screener.py +24 -0
- finbrain/client.py +20 -3
- finbrain/endpoints/_utils.py +7 -1
- finbrain/endpoints/government_contracts.py +73 -0
- finbrain/endpoints/patent_filings.py +74 -0
- finbrain/endpoints/screener.py +24 -0
- finbrain/exceptions.py +24 -0
- finbrain/plotting.py +508 -137
- {finbrain_python-0.2.3.dist-info → finbrain_python-0.2.5.dist-info}/METADATA +38 -1
- {finbrain_python-0.2.3.dist-info → finbrain_python-0.2.5.dist-info}/RECORD +17 -13
- {finbrain_python-0.2.3.dist-info → finbrain_python-0.2.5.dist-info}/WHEEL +0 -0
- {finbrain_python-0.2.3.dist-info → finbrain_python-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {finbrain_python-0.2.3.dist-info → finbrain_python-0.2.5.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
169
|
+
# Transient server/gateway error – exponential back-off then retry
|
|
166
170
|
await asyncio.sleep(_BACKOFF_BASE**attempt)
|
|
167
171
|
continue
|
|
168
172
|
|
finbrain/aio/endpoints/_utils.py
CHANGED
|
@@ -1,37 +1,12 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Shared utility functions for async endpoint modules.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
149
|
+
# Transient server/gateway error – exponential back-off then retry
|
|
133
150
|
time.sleep(_BACKOFF_BASE**attempt)
|
|
134
151
|
continue
|
|
135
152
|
|
finbrain/endpoints/_utils.py
CHANGED
|
@@ -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
|
finbrain/endpoints/screener.py
CHANGED
|
@@ -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)
|