finbrain-python 0.2.4__py3-none-any.whl → 0.2.6__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
@@ -25,10 +25,11 @@ from .endpoints.recent import AsyncRecentAPI
25
25
  from .endpoints.corporate_lobbying import AsyncCorporateLobbyingAPI
26
26
  from .endpoints.reddit_mentions import AsyncRedditMentionsAPI
27
27
  from .endpoints.government_contracts import AsyncGovernmentContractsAPI
28
+ from .endpoints.patent_filings import AsyncPatentFilingsAPI
28
29
 
29
30
 
30
- # Which status codes merit a retry
31
- _RETRYABLE_STATUS = {500}
31
+ # Which status codes merit a retry (transient server / gateway errors)
32
+ _RETRYABLE_STATUS = {500, 502, 503, 504}
32
33
  # How long to wait between retries (2, 4, 8 … seconds)
33
34
  _BACKOFF_BASE = 2
34
35
 
@@ -86,6 +87,7 @@ class AsyncFinBrainClient:
86
87
  self.corporate_lobbying = AsyncCorporateLobbyingAPI(self)
87
88
  self.reddit_mentions = AsyncRedditMentionsAPI(self)
88
89
  self.government_contracts = AsyncGovernmentContractsAPI(self)
90
+ self.patent_filings = AsyncPatentFilingsAPI(self)
89
91
 
90
92
  async def __aenter__(self) -> "AsyncFinBrainClient":
91
93
  """Context manager entry."""
@@ -164,7 +166,7 @@ class AsyncFinBrainClient:
164
166
 
165
167
  # ── Error path ───────────────────────────────────
166
168
  if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
167
- # 500 – exponential back-off then retry
169
+ # Transient server/gateway error – exponential back-off then retry
168
170
  await asyncio.sleep(_BACKOFF_BASE**attempt)
169
171
  continue
170
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"]
@@ -24,7 +24,19 @@ class AsyncHouseTradesAPI:
24
24
  limit: int | None = None,
25
25
  as_dataframe: bool = False,
26
26
  ) -> Dict[str, Any] | pd.DataFrame:
27
- """Fetch House-member trades for a symbol (async)."""
27
+ """
28
+ Fetch House-member trades for a symbol (async).
29
+
30
+ Each row in ``trades`` carries ``date`` (the transaction date),
31
+ ``politician``, ``transactionType``, ``amount`` and
32
+ ``disclosureDate`` — the date the trade was publicly disclosed in the
33
+ member's periodic transaction report. ``disclosureDate`` is ``None``
34
+ for historical rows collected before the field was captured (``NaN``
35
+ in the DataFrame branch on newer pandas — use ``pandas.isna``).
36
+
37
+ ``date_from``/``date_to`` bound the transaction date, not the
38
+ disclosure date.
39
+ """
28
40
  params: Dict[str, str] = {}
29
41
  if date_from:
30
42
  params["startDate"] = to_datestr(date_from)
@@ -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
@@ -107,7 +107,13 @@ class AsyncScreenerAPI:
107
107
  limit: int | None = None,
108
108
  as_dataframe: bool = False,
109
109
  ) -> List[Dict[str, Any]] | pd.DataFrame:
110
- """Screen House trades across all tickers."""
110
+ """
111
+ Screen House trades across all tickers.
112
+
113
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
114
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
115
+ disclosure date, ``None`` on historical rows).
116
+ """
111
117
  params = self._build_params(limit=limit)
112
118
  return await self._get("screener/congress/house", params, as_dataframe)
113
119
 
@@ -119,7 +125,13 @@ class AsyncScreenerAPI:
119
125
  limit: int | None = None,
120
126
  as_dataframe: bool = False,
121
127
  ) -> List[Dict[str, Any]] | pd.DataFrame:
122
- """Screen Senate trades across all tickers."""
128
+ """
129
+ Screen Senate trades across all tickers.
130
+
131
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
132
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
133
+ disclosure date, ``None`` on historical rows).
134
+ """
123
135
  params = self._build_params(limit=limit)
124
136
  return await self._get("screener/congress/senate", params, as_dataframe)
125
137
 
@@ -234,3 +246,15 @@ class AsyncScreenerAPI:
234
246
  """Screen government contracts across all tickers (async)."""
235
247
  params = self._build_params(limit=limit)
236
248
  return await self._get("screener/government-contracts", params, as_dataframe)
249
+
250
+ # ── patent filings ────────────────────────────────────────
251
+
252
+ async def patent_filings(
253
+ self,
254
+ *,
255
+ limit: int | None = None,
256
+ as_dataframe: bool = False,
257
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
258
+ """Screen USPTO patent filings across all tickers (async)."""
259
+ params = self._build_params(limit=limit)
260
+ return await self._get("screener/patent-filings", params, as_dataframe)
@@ -24,7 +24,19 @@ class AsyncSenateTradesAPI:
24
24
  limit: int | None = None,
25
25
  as_dataframe: bool = False,
26
26
  ) -> Dict[str, Any] | pd.DataFrame:
27
- """Fetch Senate-member trades for a symbol (async)."""
27
+ """
28
+ Fetch Senate-member trades for a symbol (async).
29
+
30
+ Each row in ``trades`` carries ``date`` (the transaction date),
31
+ ``politician``, ``transactionType``, ``amount`` and
32
+ ``disclosureDate`` — the date the trade was publicly disclosed in the
33
+ member's periodic transaction report. ``disclosureDate`` is ``None``
34
+ for historical rows collected before the field was captured (``NaN``
35
+ in the DataFrame branch on newer pandas — use ``pandas.isna``).
36
+
37
+ ``date_from``/``date_to`` bound the transaction date, not the
38
+ disclosure date.
39
+ """
28
40
  params: Dict[str, str] = {}
29
41
  if date_from:
30
42
  params["startDate"] = to_datestr(date_from)
finbrain/client.py CHANGED
@@ -26,10 +26,11 @@ from .endpoints.recent import RecentAPI
26
26
  from .endpoints.corporate_lobbying import CorporateLobbyingAPI
27
27
  from .endpoints.reddit_mentions import RedditMentionsAPI
28
28
  from .endpoints.government_contracts import GovernmentContractsAPI
29
+ from .endpoints.patent_filings import PatentFilingsAPI
29
30
 
30
31
 
31
- # Which status codes merit a retry
32
- _RETRYABLE_STATUS = {500}
32
+ # Which status codes merit a retry (transient server / gateway errors)
33
+ _RETRYABLE_STATUS = {500, 502, 503, 504}
33
34
  # How long to wait between retries (2, 4, 8 … seconds)
34
35
  _BACKOFF_BASE = 2
35
36
 
@@ -81,6 +82,20 @@ class FinBrainClient:
81
82
  self.corporate_lobbying = CorporateLobbyingAPI(self)
82
83
  self.reddit_mentions = RedditMentionsAPI(self)
83
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()
84
99
 
85
100
  # ---------- private helpers ----------
86
101
  def _request(
@@ -131,7 +146,7 @@ class FinBrainClient:
131
146
 
132
147
  # ── Error path ───────────────────────────────────
133
148
  if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
134
- # 500 – exponential back-off then retry
149
+ # Transient server/gateway error – exponential back-off then retry
135
150
  time.sleep(_BACKOFF_BASE**attempt)
136
151
  continue
137
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
@@ -49,6 +49,30 @@ class HouseTradesAPI:
49
49
  Returns
50
50
  -------
51
51
  dict | pandas.DataFrame
52
+ The raw dict has a ``trades`` list whose rows carry ``date``
53
+ (the transaction date), ``politician``, ``transactionType``,
54
+ ``amount`` and ``disclosureDate`` — the date the trade was
55
+ publicly disclosed in the member's periodic transaction report.
56
+ The gap between the two dates is the reporting lag.
57
+
58
+ ``disclosureDate`` is ``None`` for historical rows collected
59
+ before the field was captured upstream. In the DataFrame branch
60
+ ``date`` becomes the index and ``disclosureDate`` is a column
61
+ whose missing values read as ``None`` or ``NaN`` depending on the
62
+ pandas version — test them with :func:`pandas.isna`.
63
+
64
+ Notes
65
+ -----
66
+ ``date_from`` and ``date_to`` bound the **transaction** date, not the
67
+ disclosure date. A trade executed inside the window is returned even
68
+ if it was disclosed after ``date_to``.
69
+
70
+ Example row::
71
+
72
+ {"date": "2026-06-15", "politician": "Jane Doe",
73
+ "transactionType": "Purchase", "amount": "$1,001 - $15,000",
74
+ "disclosureDate": "2026-07-01"}
75
+
52
76
  """
53
77
  params: Dict[str, str] = {}
54
78
  if date_from:
@@ -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
@@ -112,7 +112,13 @@ class ScreenerAPI:
112
112
  limit: int | None = None,
113
113
  as_dataframe: bool = False,
114
114
  ) -> List[Dict[str, Any]] | pd.DataFrame:
115
- """Screen House trades across all tickers."""
115
+ """
116
+ Screen House trades across all tickers.
117
+
118
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
119
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
120
+ disclosure date, ``None`` on historical rows).
121
+ """
116
122
  params = self._build_params(limit=limit)
117
123
  return self._get("screener/congress/house", params, as_dataframe)
118
124
 
@@ -124,7 +130,13 @@ class ScreenerAPI:
124
130
  limit: int | None = None,
125
131
  as_dataframe: bool = False,
126
132
  ) -> List[Dict[str, Any]] | pd.DataFrame:
127
- """Screen Senate trades across all tickers."""
133
+ """
134
+ Screen Senate trades across all tickers.
135
+
136
+ Rows carry ``symbol``, ``name``, ``date``, ``politician``,
137
+ ``transactionType``, ``amount`` and ``disclosureDate`` (the public
138
+ disclosure date, ``None`` on historical rows).
139
+ """
128
140
  params = self._build_params(limit=limit)
129
141
  return self._get("screener/congress/senate", params, as_dataframe)
130
142
 
@@ -239,3 +251,15 @@ class ScreenerAPI:
239
251
  """Screen government contracts across all tickers."""
240
252
  params = self._build_params(limit=limit)
241
253
  return self._get("screener/government-contracts", params, as_dataframe)
254
+
255
+ # ── patent filings ────────────────────────────────────────
256
+
257
+ def patent_filings(
258
+ self,
259
+ *,
260
+ limit: int | None = None,
261
+ as_dataframe: bool = False,
262
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
263
+ """Screen USPTO patent filings across all tickers."""
264
+ params = self._build_params(limit=limit)
265
+ return self._get("screener/patent-filings", params, as_dataframe)
@@ -49,6 +49,30 @@ class SenateTradesAPI:
49
49
  Returns
50
50
  -------
51
51
  dict | pandas.DataFrame
52
+ The raw dict has a ``trades`` list whose rows carry ``date``
53
+ (the transaction date), ``politician``, ``transactionType``,
54
+ ``amount`` and ``disclosureDate`` — the date the trade was
55
+ publicly disclosed in the member's periodic transaction report.
56
+ The gap between the two dates is the reporting lag.
57
+
58
+ ``disclosureDate`` is ``None`` for historical rows collected
59
+ before the field was captured upstream. In the DataFrame branch
60
+ ``date`` becomes the index and ``disclosureDate`` is a column
61
+ whose missing values read as ``None`` or ``NaN`` depending on the
62
+ pandas version — test them with :func:`pandas.isna`.
63
+
64
+ Notes
65
+ -----
66
+ ``date_from`` and ``date_to`` bound the **transaction** date, not the
67
+ disclosure date. A trade executed inside the window is returned even
68
+ if it was disclosed after ``date_to``.
69
+
70
+ Example row::
71
+
72
+ {"date": "2026-06-10", "politician": "Jane Doe",
73
+ "transactionType": "Purchase", "amount": "$1,001 - $15,000",
74
+ "disclosureDate": "2026-06-25"}
75
+
52
76
  """
53
77
  params: Dict[str, str] = {}
54
78
  if date_from:
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)