finbrain-python 0.2.4__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
@@ -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"]
@@ -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
@@ -234,3 +234,15 @@ class AsyncScreenerAPI:
234
234
  """Screen government contracts across all tickers (async)."""
235
235
  params = self._build_params(limit=limit)
236
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
@@ -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
@@ -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
@@ -239,3 +239,15 @@ class ScreenerAPI:
239
239
  """Screen government contracts across all tickers."""
240
240
  params = self._build_params(limit=limit)
241
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)
finbrain/plotting.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # src/finbrain/plotting.py
2
2
  from __future__ import annotations
3
- from typing import Union, TYPE_CHECKING
3
+ from typing import Literal, Union, TYPE_CHECKING
4
4
  import numpy as np
5
5
  import pandas as pd
6
6
  import plotly.graph_objects as go
@@ -46,7 +46,7 @@ class _PlotNamespace:
46
46
  Other args/kwargs identical to the other plotting wrappers.
47
47
  """
48
48
  # 1) pull data
49
- df = self._fb.app_ratings.ticker(
49
+ df: pd.DataFrame = self._fb.app_ratings.ticker(
50
50
  ticker,
51
51
  date_from=date_from,
52
52
  date_to=date_to,
@@ -150,7 +150,7 @@ class _PlotNamespace:
150
150
  * **Bars** → ``employeeCount`` (primary y-axis)
151
151
  * **Line** → ``followerCount`` (secondary y-axis)
152
152
  """
153
- df = self._fb.linkedin_data.ticker(
153
+ df: pd.DataFrame = self._fb.linkedin_data.ticker(
154
154
  ticker,
155
155
  date_from=date_from,
156
156
  date_to=date_to,
@@ -344,7 +344,7 @@ class _PlotNamespace:
344
344
  ... date_to="2025-05-31")
345
345
  """
346
346
  if kind == "put_call":
347
- df = self._fb.options.put_call(
347
+ df: pd.DataFrame = self._fb.options.put_call(
348
348
  ticker,
349
349
  date_from=date_from,
350
350
  date_to=date_to,
@@ -368,7 +368,7 @@ class _PlotNamespace:
368
368
  self,
369
369
  ticker: str,
370
370
  *,
371
- prediction_type: str = "daily",
371
+ prediction_type: Literal["daily", "monthly"] = "daily",
372
372
  as_json=False,
373
373
  show=True,
374
374
  template="plotly_dark",
@@ -398,7 +398,7 @@ class _PlotNamespace:
398
398
  --------
399
399
  >>> fb.plot.predictions("AMZN", prediction_type="monthly")
400
400
  """
401
- df = self._fb.predictions.ticker(
401
+ df: pd.DataFrame = self._fb.predictions.ticker(
402
402
  ticker, prediction_type=prediction_type, as_dataframe=True, **kw
403
403
  )
404
404
 
@@ -496,30 +496,10 @@ class _PlotNamespace:
496
496
  ... }).set_index("date")
497
497
  >>> fb.plot.insider_transactions("AAPL", price_df)
498
498
  """
499
- # Validate price_data
500
- if price_data.empty:
501
- raise ValueError("price_data cannot be empty")
502
-
503
- # Flatten MultiIndex columns if present (e.g., from yf.download())
504
- if isinstance(price_data.columns, pd.MultiIndex):
505
- # Get the first level (price types like 'Close', 'Open', etc.)
506
- price_data = price_data.copy()
507
- price_data.columns = price_data.columns.get_level_values(0)
508
-
509
- # Find price column (case-insensitive search)
510
- price_col = None
511
- for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
512
- if col in price_data.columns:
513
- price_col = col
514
- break
515
- if price_col is None:
516
- raise ValueError(
517
- f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
518
- f"Found columns: {price_data.columns.tolist()}"
519
- )
499
+ price_data, price_col = self._resolve_price_column(price_data)
520
500
 
521
501
  # Fetch insider transactions
522
- transactions_df = self._fb.insider_transactions.ticker(
502
+ transactions_df: pd.DataFrame = self._fb.insider_transactions.ticker(
523
503
  ticker, as_dataframe=True, **kwargs
524
504
  )
525
505
 
@@ -599,30 +579,10 @@ class _PlotNamespace:
599
579
  >>> fb.plot.house_trades("AAPL", price_df,
600
580
  ... date_from="2024-01-01", date_to="2024-12-31")
601
581
  """
602
- # Validate price_data
603
- if price_data.empty:
604
- raise ValueError("price_data cannot be empty")
605
-
606
- # Flatten MultiIndex columns if present (e.g., from yf.download())
607
- if isinstance(price_data.columns, pd.MultiIndex):
608
- # Get the first level (price types like 'Close', 'Open', etc.)
609
- price_data = price_data.copy()
610
- price_data.columns = price_data.columns.get_level_values(0)
611
-
612
- # Find price column (case-insensitive search)
613
- price_col = None
614
- for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
615
- if col in price_data.columns:
616
- price_col = col
617
- break
618
- if price_col is None:
619
- raise ValueError(
620
- f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
621
- f"Found columns: {price_data.columns.tolist()}"
622
- )
582
+ price_data, price_col = self._resolve_price_column(price_data)
623
583
 
624
584
  # Fetch house trades
625
- transactions_df = self._fb.house_trades.ticker(
585
+ transactions_df: pd.DataFrame = self._fb.house_trades.ticker(
626
586
  ticker,
627
587
  date_from=date_from,
628
588
  date_to=date_to,
@@ -706,30 +666,10 @@ class _PlotNamespace:
706
666
  >>> fb.plot.senate_trades("META", price_df,
707
667
  ... date_from="2024-01-01", date_to="2024-12-31")
708
668
  """
709
- # Validate price_data
710
- if price_data.empty:
711
- raise ValueError("price_data cannot be empty")
712
-
713
- # Flatten MultiIndex columns if present (e.g., from yf.download())
714
- if isinstance(price_data.columns, pd.MultiIndex):
715
- # Get the first level (price types like 'Close', 'Open', etc.)
716
- price_data = price_data.copy()
717
- price_data.columns = price_data.columns.get_level_values(0)
718
-
719
- # Find price column (case-insensitive search)
720
- price_col = None
721
- for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
722
- if col in price_data.columns:
723
- price_col = col
724
- break
725
- if price_col is None:
726
- raise ValueError(
727
- f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
728
- f"Found columns: {price_data.columns.tolist()}"
729
- )
669
+ price_data, price_col = self._resolve_price_column(price_data)
730
670
 
731
671
  # Fetch senate trades
732
- transactions_df = self._fb.senate_trades.ticker(
672
+ transactions_df: pd.DataFrame = self._fb.senate_trades.ticker(
733
673
  ticker,
734
674
  date_from=date_from,
735
675
  date_to=date_to,
@@ -802,29 +742,10 @@ class _PlotNamespace:
802
742
  ValueError
803
743
  If ``price_data`` is empty or missing required price column.
804
744
  """
805
- # Validate price_data
806
- if price_data.empty:
807
- raise ValueError("price_data cannot be empty")
808
-
809
- # Flatten MultiIndex columns if present (e.g., from yf.download())
810
- if isinstance(price_data.columns, pd.MultiIndex):
811
- price_data = price_data.copy()
812
- price_data.columns = price_data.columns.get_level_values(0)
813
-
814
- # Find price column (case-insensitive search)
815
- price_col = None
816
- for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
817
- if col in price_data.columns:
818
- price_col = col
819
- break
820
- if price_col is None:
821
- raise ValueError(
822
- f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
823
- f"Found columns: {price_data.columns.tolist()}"
824
- )
745
+ price_data, price_col = self._resolve_price_column(price_data)
825
746
 
826
747
  # Fetch lobbying filings
827
- filings_df = self._fb.corporate_lobbying.ticker(
748
+ filings_df: pd.DataFrame = self._fb.corporate_lobbying.ticker(
828
749
  ticker,
829
750
  date_from=date_from,
830
751
  date_to=date_to,
@@ -833,9 +754,7 @@ class _PlotNamespace:
833
754
  )
834
755
 
835
756
  # Normalize timezones
836
- price_data_normalized = price_data.copy()
837
- if price_data_normalized.index.tz is not None:
838
- price_data_normalized.index = price_data_normalized.index.tz_localize(None)
757
+ price_data_normalized = self._to_naive_index(price_data)
839
758
 
840
759
  fig = go.Figure(
841
760
  layout=dict(
@@ -857,9 +776,7 @@ class _PlotNamespace:
857
776
  )
858
777
 
859
778
  if not filings_df.empty:
860
- filings_normalized = filings_df.copy()
861
- if filings_normalized.index.tz is not None:
862
- filings_normalized.index = filings_normalized.index.tz_localize(None)
779
+ filings_normalized = self._to_naive_index(filings_df)
863
780
 
864
781
  # Compute total spend per filing (income + expenses)
865
782
  spend = filings_normalized.get("income", 0) + filings_normalized.get(
@@ -958,29 +875,10 @@ class _PlotNamespace:
958
875
  ValueError
959
876
  If ``price_data`` is empty or missing required price column.
960
877
  """
961
- # Validate price_data
962
- if price_data.empty:
963
- raise ValueError("price_data cannot be empty")
964
-
965
- # Flatten MultiIndex columns if present (e.g., from yf.download())
966
- if isinstance(price_data.columns, pd.MultiIndex):
967
- price_data = price_data.copy()
968
- price_data.columns = price_data.columns.get_level_values(0)
969
-
970
- # Find price column (case-insensitive search)
971
- price_col = None
972
- for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
973
- if col in price_data.columns:
974
- price_col = col
975
- break
976
- if price_col is None:
977
- raise ValueError(
978
- f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
979
- f"Found columns: {price_data.columns.tolist()}"
980
- )
878
+ price_data, price_col = self._resolve_price_column(price_data)
981
879
 
982
880
  # Fetch Reddit mentions
983
- mentions_df = self._fb.reddit_mentions.ticker(
881
+ mentions_df: pd.DataFrame = self._fb.reddit_mentions.ticker(
984
882
  ticker,
985
883
  date_from=date_from,
986
884
  date_to=date_to,
@@ -989,9 +887,7 @@ class _PlotNamespace:
989
887
  )
990
888
 
991
889
  # Normalize timezones
992
- price_data_normalized = price_data.copy()
993
- if price_data_normalized.index.tz is not None:
994
- price_data_normalized.index = price_data_normalized.index.tz_localize(None)
890
+ price_data_normalized = self._to_naive_index(price_data)
995
891
 
996
892
  fig = go.Figure(
997
893
  layout=dict(
@@ -1013,9 +909,7 @@ class _PlotNamespace:
1013
909
  )
1014
910
 
1015
911
  if not mentions_df.empty:
1016
- mentions_normalized = mentions_df.copy()
1017
- if mentions_normalized.index.tz is not None:
1018
- mentions_normalized.index = mentions_normalized.index.tz_localize(None)
912
+ mentions_normalized = self._to_naive_index(mentions_df)
1019
913
 
1020
914
  # Exclude _all (aggregate) — use individual subreddits for stacked bars
1021
915
  per_sub = mentions_normalized[
@@ -1205,29 +1099,10 @@ class _PlotNamespace:
1205
1099
  ValueError
1206
1100
  If ``price_data`` is empty or missing required price column.
1207
1101
  """
1208
- # Validate price_data
1209
- if price_data.empty:
1210
- raise ValueError("price_data cannot be empty")
1211
-
1212
- # Flatten MultiIndex columns if present (e.g., from yf.download())
1213
- if isinstance(price_data.columns, pd.MultiIndex):
1214
- price_data = price_data.copy()
1215
- price_data.columns = price_data.columns.get_level_values(0)
1216
-
1217
- # Find price column (case-insensitive search)
1218
- price_col = None
1219
- for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
1220
- if col in price_data.columns:
1221
- price_col = col
1222
- break
1223
- if price_col is None:
1224
- raise ValueError(
1225
- f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
1226
- f"Found columns: {price_data.columns.tolist()}"
1227
- )
1102
+ price_data, price_col = self._resolve_price_column(price_data)
1228
1103
 
1229
1104
  # Fetch government contracts
1230
- contracts_df = self._fb.government_contracts.ticker(
1105
+ contracts_df: pd.DataFrame = self._fb.government_contracts.ticker(
1231
1106
  ticker,
1232
1107
  date_from=date_from,
1233
1108
  date_to=date_to,
@@ -1236,9 +1111,7 @@ class _PlotNamespace:
1236
1111
  )
1237
1112
 
1238
1113
  # Normalize timezones
1239
- price_data_normalized = price_data.copy()
1240
- if price_data_normalized.index.tz is not None:
1241
- price_data_normalized.index = price_data_normalized.index.tz_localize(None)
1114
+ price_data_normalized = self._to_naive_index(price_data)
1242
1115
 
1243
1116
  fig = go.Figure(
1244
1117
  layout=dict(
@@ -1260,9 +1133,7 @@ class _PlotNamespace:
1260
1133
  )
1261
1134
 
1262
1135
  if not contracts_df.empty:
1263
- contracts_normalized = contracts_df.copy()
1264
- if contracts_normalized.index.tz is not None:
1265
- contracts_normalized.index = contracts_normalized.index.tz_localize(None)
1136
+ contracts_normalized = self._to_naive_index(contracts_df)
1266
1137
 
1267
1138
  amounts = contracts_normalized.get("awardAmount", pd.Series(dtype=float))
1268
1139
 
@@ -1308,12 +1179,365 @@ class _PlotNamespace:
1308
1179
  return None
1309
1180
  return fig.to_json() if as_json else fig
1310
1181
 
1182
+ def patent_filings(
1183
+ self,
1184
+ ticker: str,
1185
+ price_data: pd.DataFrame,
1186
+ *,
1187
+ date_from: str | None = None,
1188
+ date_to: str | None = None,
1189
+ as_json: bool = False,
1190
+ show: bool = True,
1191
+ template: str = "plotly_dark",
1192
+ **kwargs,
1193
+ ):
1194
+ """
1195
+ Plot USPTO granted patents overlaid on a price chart.
1196
+
1197
+ Each granted patent is drawn as a bar (sized by its claim count) on a
1198
+ secondary y-axis, positioned at the patent's grant date. This method
1199
+ requires user-provided historical price data, as FinBrain does not
1200
+ currently offer a price history endpoint.
1201
+
1202
+ Parameters
1203
+ ----------
1204
+ ticker : str
1205
+ Ticker symbol (e.g. ``"AAPL"``).
1206
+ price_data : pandas.DataFrame
1207
+ **User-provided** price history with a DatetimeIndex and a column
1208
+ containing prices (e.g. ``"close"``, ``"Close"``, or ``"price"``).
1209
+ The index must be timezone-naive or UTC.
1210
+ date_from, date_to : str or None, optional
1211
+ Date range for patents in ``YYYY-MM-DD`` format (filters grant date).
1212
+ as_json : bool, default False
1213
+ If ``True``, return JSON string instead of Figure object.
1214
+ show : bool, default True
1215
+ If ``True`` and ``as_json=False``, display the figure immediately.
1216
+ template : str, default "plotly_dark"
1217
+ Plotly template name.
1218
+ **kwargs
1219
+ Additional arguments passed to
1220
+ :meth:`FinBrainClient.patent_filings.ticker`.
1221
+
1222
+ Returns
1223
+ -------
1224
+ plotly.graph_objects.Figure or str or None
1225
+ Figure object, JSON string, or None (when shown).
1226
+
1227
+ Raises
1228
+ ------
1229
+ ValueError
1230
+ If ``price_data`` is empty or missing required price column.
1231
+ """
1232
+ price_data, price_col = self._resolve_price_column(price_data)
1233
+
1234
+ # Fetch patent filings
1235
+ patents_df: pd.DataFrame = self._fb.patent_filings.ticker(
1236
+ ticker,
1237
+ date_from=date_from,
1238
+ date_to=date_to,
1239
+ as_dataframe=True,
1240
+ **kwargs,
1241
+ )
1242
+
1243
+ # Normalize timezones
1244
+ price_data_normalized = self._to_naive_index(price_data)
1245
+
1246
+ fig = go.Figure(
1247
+ layout=dict(
1248
+ template=template,
1249
+ title=f"Patent Filings · {ticker}",
1250
+ xaxis_title="Date",
1251
+ hovermode="x unified",
1252
+ )
1253
+ )
1254
+
1255
+ # Plot price line on primary y-axis
1256
+ fig.add_scatter(
1257
+ name="Price",
1258
+ x=price_data_normalized.index,
1259
+ y=price_data_normalized[price_col],
1260
+ mode="lines",
1261
+ line=dict(width=2, color="#02d2ff"),
1262
+ hovertemplate="<b>%{x|%Y-%m-%d}</b><br>Price: $%{y:.2f}<extra></extra>",
1263
+ )
1264
+
1265
+ if not patents_df.empty:
1266
+ patents_normalized = self._to_naive_index(patents_df)
1267
+
1268
+ claims = patents_normalized.get("numClaims", pd.Series(dtype=float))
1269
+
1270
+ hover_text = []
1271
+ for _, row in patents_normalized.iterrows():
1272
+ title = row.get("title", "")
1273
+ if len(str(title)) > 80:
1274
+ title = str(title)[:80] + "…"
1275
+ ptype = row.get("type", "N/A")
1276
+ section = row.get("primaryCpcSection", "")
1277
+ n_claims = row.get("numClaims", 0)
1278
+ hover_text.append(
1279
+ f"Title: {title}<br>"
1280
+ f"Type: {ptype}<br>"
1281
+ f"CPC Section: {section}<br>"
1282
+ f"Claims: {n_claims}"
1283
+ )
1284
+
1285
+ fig.add_bar(
1286
+ name="Patent Grant",
1287
+ x=patents_normalized.index,
1288
+ y=claims,
1289
+ marker_color="rgba(249,200,14,0.6)",
1290
+ yaxis="y2",
1291
+ hovertext=hover_text,
1292
+ hovertemplate="<b>%{x|%Y-%m-%d}</b><br>%{hovertext}<extra></extra>",
1293
+ )
1294
+
1295
+ fig.update_layout(
1296
+ yaxis=dict(title="Price", showgrid=True),
1297
+ yaxis2=dict(
1298
+ title="Claims",
1299
+ overlaying="y",
1300
+ side="right",
1301
+ showgrid=False,
1302
+ zeroline=False,
1303
+ rangemode="tozero",
1304
+ ),
1305
+ )
1306
+
1307
+ if show and not as_json:
1308
+ fig.show()
1309
+ return None
1310
+ return fig.to_json() if as_json else fig
1311
+
1312
+ def analyst_ratings(
1313
+ self,
1314
+ ticker: str,
1315
+ price_data: pd.DataFrame,
1316
+ *,
1317
+ date_from: str | None = None,
1318
+ date_to: str | None = None,
1319
+ as_json: bool = False,
1320
+ show: bool = True,
1321
+ template: str = "plotly_dark",
1322
+ **kwargs,
1323
+ ):
1324
+ """
1325
+ Plot analyst rating actions and price targets overlaid on a price chart.
1326
+
1327
+ Each rating is drawn as a marker at its **target price** (where one is
1328
+ provided) or, when no target is given, at the prevailing price on that
1329
+ date, so every action remains visible. Markers are grouped and coloured
1330
+ by action category (upgrade, downgrade, initiate, maintain, other). This
1331
+ method requires user-provided historical price data, as FinBrain does
1332
+ not currently offer a price history endpoint.
1333
+
1334
+ Parameters
1335
+ ----------
1336
+ ticker : str
1337
+ Ticker symbol (e.g. ``"AAPL"``).
1338
+ price_data : pandas.DataFrame
1339
+ **User-provided** price history with a DatetimeIndex and a column
1340
+ containing prices (e.g. ``"close"``, ``"Close"``, or ``"price"``).
1341
+ The index must be timezone-naive or UTC.
1342
+ date_from, date_to : str or None, optional
1343
+ Date range for ratings in ``YYYY-MM-DD`` format.
1344
+ as_json : bool, default False
1345
+ If ``True``, return JSON string instead of Figure object.
1346
+ show : bool, default True
1347
+ If ``True`` and ``as_json=False``, display the figure immediately.
1348
+ template : str, default "plotly_dark"
1349
+ Plotly template name.
1350
+ **kwargs
1351
+ Additional arguments passed to
1352
+ :meth:`FinBrainClient.analyst_ratings.ticker`.
1353
+
1354
+ Returns
1355
+ -------
1356
+ plotly.graph_objects.Figure or str or None
1357
+ Figure object, JSON string, or None (when shown).
1358
+
1359
+ Raises
1360
+ ------
1361
+ ValueError
1362
+ If ``price_data`` is empty or missing required price column.
1363
+ """
1364
+ price_data, price_col = self._resolve_price_column(price_data)
1365
+
1366
+ # Fetch analyst ratings
1367
+ ratings_df: pd.DataFrame = self._fb.analyst_ratings.ticker(
1368
+ ticker,
1369
+ date_from=date_from,
1370
+ date_to=date_to,
1371
+ as_dataframe=True,
1372
+ **kwargs,
1373
+ )
1374
+
1375
+ # Normalize timezones
1376
+ price_data_normalized = self._to_naive_index(price_data)
1377
+
1378
+ fig = go.Figure(
1379
+ layout=dict(
1380
+ template=template,
1381
+ title=f"Analyst Ratings · {ticker}",
1382
+ xaxis_title="Date",
1383
+ yaxis_title="Price / Target ($)",
1384
+ hovermode="x unified",
1385
+ )
1386
+ )
1387
+
1388
+ # Plot price line on primary y-axis
1389
+ fig.add_scatter(
1390
+ name="Price",
1391
+ x=price_data_normalized.index,
1392
+ y=price_data_normalized[price_col],
1393
+ mode="lines",
1394
+ line=dict(width=2, color="#02d2ff"),
1395
+ hovertemplate="<b>%{x|%Y-%m-%d}</b><br>Price: $%{y:.2f}<extra></extra>",
1396
+ )
1397
+
1398
+ if not ratings_df.empty:
1399
+ ratings_normalized = self._to_naive_index(ratings_df)
1400
+
1401
+ targets = pd.to_numeric(
1402
+ ratings_normalized.get("targetPrice"), errors="coerce"
1403
+ )
1404
+
1405
+ def _price_at(when):
1406
+ """Nearest available price for a rating date."""
1407
+ if when in price_data_normalized.index:
1408
+ return price_data_normalized.loc[when, price_col]
1409
+ idx = price_data_normalized.index.get_indexer([when], method="nearest")[0]
1410
+ if 0 <= idx < len(price_data_normalized):
1411
+ return price_data_normalized.iloc[idx][price_col]
1412
+ return None
1413
+
1414
+ # action category → (legend label, colour)
1415
+ categories = {
1416
+ "upgrade": ("Upgrade", "#26a69a"),
1417
+ "downgrade": ("Downgrade", "#ef5350"),
1418
+ "initiate": ("Initiate", "#42a5f5"),
1419
+ "maintain": ("Maintain", "#bdbdbd"),
1420
+ "other": ("Other", "#f9c80e"),
1421
+ }
1422
+
1423
+ def _categorize(action: str) -> str:
1424
+ a = str(action).lower()
1425
+ if "upgrade" in a:
1426
+ return "upgrade"
1427
+ if "downgrade" in a:
1428
+ return "downgrade"
1429
+ if "initiat" in a:
1430
+ return "initiate"
1431
+ if any(k in a for k in ("maintain", "reiterat", "reaffirm", "hold")):
1432
+ return "maintain"
1433
+ return "other"
1434
+
1435
+ # Bucket each rating row by action category
1436
+ buckets: dict[str, dict[str, list]] = {
1437
+ key: {"x": [], "y": [], "symbol": [], "hover": []}
1438
+ for key in categories
1439
+ }
1440
+
1441
+ for pos, (when, row) in enumerate(ratings_normalized.iterrows()):
1442
+ target = targets.iloc[pos]
1443
+ has_target = pd.notna(target)
1444
+ y_val = target if has_target else _price_at(when)
1445
+ if y_val is None:
1446
+ continue
1447
+
1448
+ cat = _categorize(row.get("action", ""))
1449
+ bucket = buckets[cat]
1450
+ bucket["x"].append(when)
1451
+ bucket["y"].append(y_val)
1452
+ # diamond = plotted at target, open circle = plotted at price
1453
+ bucket["symbol"].append("diamond" if has_target else "circle-open")
1454
+ target_str = f"${target:,.2f}" if has_target else "n/a"
1455
+ bucket["hover"].append(
1456
+ f"Institution: {row.get('institution', 'N/A')}<br>"
1457
+ f"Action: {row.get('action', 'N/A')}<br>"
1458
+ f"Rating: {row.get('rating', 'N/A')}<br>"
1459
+ f"Target: {target_str}"
1460
+ )
1461
+
1462
+ for key, (label, color) in categories.items():
1463
+ bucket = buckets[key]
1464
+ if not bucket["x"]:
1465
+ continue
1466
+ fig.add_scatter(
1467
+ name=label,
1468
+ x=bucket["x"],
1469
+ y=bucket["y"],
1470
+ mode="markers",
1471
+ marker=dict(
1472
+ size=10,
1473
+ color=color,
1474
+ symbol=bucket["symbol"],
1475
+ line=dict(width=1, color="#000000"),
1476
+ ),
1477
+ hovertext=bucket["hover"],
1478
+ hovertemplate="<b>%{x|%Y-%m-%d}</b><br>%{hovertext}<extra></extra>",
1479
+ )
1480
+
1481
+ if show and not as_json:
1482
+ fig.show()
1483
+ return None
1484
+ return fig.to_json() if as_json else fig
1485
+
1311
1486
  # --------------------------------------------------------------------- #
1312
1487
  # Helper methods #
1313
1488
  # --------------------------------------------------------------------- #
1314
1489
 
1315
1490
  @staticmethod
1491
+ def _resolve_price_column(price_data: pd.DataFrame) -> tuple[pd.DataFrame, str]:
1492
+ """
1493
+ Validate ``price_data`` and locate its price column.
1494
+
1495
+ Flattens a MultiIndex column layout (e.g. from ``yf.download()``) and
1496
+ searches for a known price column case-insensitively.
1497
+
1498
+ Returns
1499
+ -------
1500
+ tuple[pandas.DataFrame, str]
1501
+ The (possibly flattened) frame and the resolved price column name.
1502
+
1503
+ Raises
1504
+ ------
1505
+ ValueError
1506
+ If ``price_data`` is empty or has no recognised price column.
1507
+ """
1508
+ if price_data.empty:
1509
+ raise ValueError("price_data cannot be empty")
1510
+
1511
+ # Flatten MultiIndex columns if present (e.g., from yf.download())
1512
+ if isinstance(price_data.columns, pd.MultiIndex):
1513
+ price_data = price_data.copy()
1514
+ price_data.columns = price_data.columns.get_level_values(0)
1515
+
1516
+ for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
1517
+ if col in price_data.columns:
1518
+ return price_data, col
1519
+
1520
+ raise ValueError(
1521
+ f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
1522
+ f"Found columns: {price_data.columns.tolist()}"
1523
+ )
1524
+
1525
+ @staticmethod
1526
+ def _to_naive_index(df: pd.DataFrame) -> pd.DataFrame:
1527
+ """
1528
+ Return a copy of ``df`` with a timezone-naive DatetimeIndex.
1529
+
1530
+ yfinance often returns timezone-aware data while FinBrain returns naive
1531
+ timestamps; normalising both sides lets them be compared and plotted
1532
+ together.
1533
+ """
1534
+ out = df.copy()
1535
+ if out.index.tz is not None:
1536
+ out.index = out.index.tz_localize(None)
1537
+ return out
1538
+
1316
1539
  def _plot_transactions_on_price(
1540
+ self,
1317
1541
  price_data: pd.DataFrame,
1318
1542
  price_col: str,
1319
1543
  transactions_df: pd.DataFrame,
@@ -1344,17 +1568,10 @@ class _PlotNamespace:
1344
1568
  -------
1345
1569
  go.Figure
1346
1570
  """
1347
- # Normalize timezones - convert both to timezone-naive for comparison
1348
- # yfinance often returns timezone-aware data, FinBrain returns naive
1349
- price_data_normalized = price_data.copy()
1350
- if price_data_normalized.index.tz is not None:
1351
- price_data_normalized.index = price_data_normalized.index.tz_localize(None)
1352
-
1353
- transactions_df_normalized = transactions_df.copy()
1354
- if transactions_df_normalized.index.tz is not None:
1355
- transactions_df_normalized.index = (
1356
- transactions_df_normalized.index.tz_localize(None)
1357
- )
1571
+ # Normalize timezones so yfinance (often tz-aware) and FinBrain (naive)
1572
+ # timestamps can be compared.
1573
+ price_data_normalized = self._to_naive_index(price_data)
1574
+ transactions_df_normalized = self._to_naive_index(transactions_df)
1358
1575
 
1359
1576
  fig = go.Figure(
1360
1577
  layout=dict(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finbrain-python
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Official Python client for the FinBrain API
5
5
  Author-email: Ahmet Salim Bilgin <ahmet@finbrain.tech>
6
6
  License-Expression: MIT
@@ -22,6 +22,8 @@ Requires-Dist: httpx>=0.24; extra == "dev"
22
22
  Requires-Dist: build; extra == "dev"
23
23
  Requires-Dist: twine; extra == "dev"
24
24
  Requires-Dist: ruff; extra == "dev"
25
+ Requires-Dist: mypy; extra == "dev"
26
+ Requires-Dist: types-requests; extra == "dev"
25
27
  Dynamic: license-file
26
28
 
27
29
  # FinBrain Python SDK&nbsp;<!-- omit in toc -->
@@ -112,6 +114,13 @@ fb.government_contracts.ticker("LMT",
112
114
  limit=50,
113
115
  as_dataframe=True)
114
116
 
117
+ # ---------- patent filings ----------
118
+ fb.patent_filings.ticker("AAPL",
119
+ date_from="2025-01-01",
120
+ date_to="2025-12-31",
121
+ limit=50,
122
+ as_dataframe=True)
123
+
115
124
  # ---------- insider transactions ----------
116
125
  fb.insider_transactions.ticker("AMZN", as_dataframe=True)
117
126
 
@@ -145,6 +154,7 @@ fb.screener.predictions_daily(limit=100, as_dataframe=True)
145
154
  fb.screener.insider_trading(limit=50)
146
155
  fb.screener.reddit_mentions(limit=100, as_dataframe=True)
147
156
  fb.screener.government_contracts(limit=100, as_dataframe=True)
157
+ fb.screener.patent_filings(limit=100, as_dataframe=True)
148
158
 
149
159
  # ---------- recent data ----------
150
160
  fb.recent.news(limit=100, as_dataframe=True)
@@ -265,6 +275,18 @@ fb.plot.reddit_mentions("TSLA",
265
275
  price_data=price_df,
266
276
  date_from="2026-03-01",
267
277
  date_to="2026-03-17")
278
+
279
+ # Plot patent grants (bars sized by claim count) on your price chart
280
+ fb.plot.patent_filings("AAPL",
281
+ price_data=price_df,
282
+ date_from="2024-01-01",
283
+ date_to="2025-06-30")
284
+
285
+ # Plot analyst ratings & price targets (markers coloured by action) on your price chart
286
+ fb.plot.analyst_ratings("AAPL",
287
+ price_data=price_df,
288
+ date_from="2024-01-01",
289
+ date_to="2025-06-30")
268
290
  ```
269
291
 
270
292
  ```python
@@ -321,6 +343,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
321
343
  | Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
322
344
  | Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
323
345
  | Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
346
+ | Patent filings | `client.patent_filings.ticker()` | `/patent-filings/{SYMBOL}` |
324
347
  | Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
325
348
  | LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
326
349
  | Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
@@ -329,6 +352,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
329
352
  | | `client.screener.insider_trading()` | `/screener/insider-trading` |
330
353
  | | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
331
354
  | | `client.screener.government_contracts()` | `/screener/government-contracts` |
355
+ | | `client.screener.patent_filings()` | `/screener/patent-filings` |
332
356
  | | ... and 8 more screener methods | |
333
357
  | Recent | `client.recent.news()` | `/recent/news` |
334
358
  | | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
@@ -356,6 +380,9 @@ except BadRequest as exc:
356
380
  | 405 | `MethodNotAllowed` | HTTP method not supported on endpoint |
357
381
  | 429 | `RateLimitError` | Too many requests |
358
382
  | 500 | `ServerError` | FinBrain internal error |
383
+ | 502 | `BadGateway` | Invalid response from upstream server |
384
+ | 503 | `ServiceUnavailable` | Service temporarily unavailable |
385
+ | 504 | `GatewayTimeout` | Upstream server timed out |
359
386
 
360
387
  ---
361
388
 
@@ -1,12 +1,12 @@
1
1
  finbrain/__init__.py,sha256=ULIgwHkWbj5sT0udC3SlTROhZCgm2y9BtPox-4joNn4,416
2
- finbrain/client.py,sha256=7ytNARm22V9ePOIPv6hq-UGvphSC9fsIuxQqTfL313I,5252
3
- finbrain/exceptions.py,sha256=ZkuhWfaMAqoEfhrN_6V2XPq0eY7vPCINM9S__tfgqYI,6249
4
- finbrain/plotting.py,sha256=ZDswlMAGfQzjCYNmKmcfpkljs9FYl4kOKJyy9vl1z2w,53596
2
+ finbrain/client.py,sha256=iu39iTUSPmiBcB-7gEJMvxHtkHXDSRVfq03Grw9UOVY,5850
3
+ finbrain/exceptions.py,sha256=xgn1UMirprJDhBNUVjxJUMfqKxHdFoBmkvCw3Hb5CH0,7019
4
+ finbrain/plotting.py,sha256=gvw2PuFwOy0vg3GRSfaQb5sSl6SdKH1zhY5USPShtn4,60606
5
5
  finbrain/py.typed,sha256=INFR1qO3jdMrtVPs0N0SAVKVbjKV80HQPoPshwGEpGo,27
6
6
  finbrain/aio/__init__.py,sha256=eJra9SqvLMkq5zoZfG6WFw7kd1jGpf9l-ovuPMeWvdw,125
7
- finbrain/aio/client.py,sha256=aiYDmukVDoCw9MmrPdxMyLOoPlR77il_nZO9CvP3ZHk,7171
7
+ finbrain/aio/client.py,sha256=I5oMft5T-ttBRw31-qFItlYPdIzj0qG30pNx0_SHXNA,7367
8
8
  finbrain/aio/endpoints/__init__.py,sha256=zdNSU1jzvnK6JnHxrvb3ye9KFCkA86xDTu9bBzmwrEQ,48
9
- finbrain/aio/endpoints/_utils.py,sha256=4ElYWGCcJ7t_lx02e49NvFHMt2g6DTcnwx3Mvwf3KyY,908
9
+ finbrain/aio/endpoints/_utils.py,sha256=yyritVvWL_boiHzgomY0_J3OHSr9CRr6Ihna-Chihfw,317
10
10
  finbrain/aio/endpoints/analyst_ratings.py,sha256=IYG-PU8HDOZoyyMcb_2KRxhiMVicYyaU_TOCVZPWroo,1451
11
11
  finbrain/aio/endpoints/app_ratings.py,sha256=QrJoLc7roTPRfInAEMwlr9vtxzruFJulGdB6msz7Eg0,2177
12
12
  finbrain/aio/endpoints/available.py,sha256=rQuffHowNF7FrVV5Ds-KE5iFjdz0I_vD7Qk2_zY2WLo,2315
@@ -17,14 +17,15 @@ finbrain/aio/endpoints/insider_transactions.py,sha256=EBqHdvapYWvI53PM_sQy0caN2r
17
17
  finbrain/aio/endpoints/linkedin_data.py,sha256=ucnkyNc1waiFHLxas1onPhxh9Y7ekcI_gOfUJ5gu1hI,1469
18
18
  finbrain/aio/endpoints/news.py,sha256=NJkcpoP8ngNsJdCzQab8yaXJzNe5ypYstzq3-mEHfqU,1446
19
19
  finbrain/aio/endpoints/options.py,sha256=k1XL3ktoFL2Exw6UE2XfU-EcS1Xw8BPXQzABLYXmfxk,1442
20
+ finbrain/aio/endpoints/patent_filings.py,sha256=Wumihz2X7knKZNF36X9t0UJ8mk0ydg6H7qdXFhEd4LQ,1513
20
21
  finbrain/aio/endpoints/predictions.py,sha256=BrUSmqy0v7ngXyoE_UHgqdght8qtJ8IRxAhk_s9UeR0,1285
21
22
  finbrain/aio/endpoints/recent.py,sha256=TSaAwf5w86O1plSmm4eg_gKSjSap2Z8rd2i7FAvgm90,2302
22
23
  finbrain/aio/endpoints/reddit_mentions.py,sha256=P9ToZRaRN521RJ9W2J8cBCySUsNT47ROe-8TTdVtlqA,1508
23
- finbrain/aio/endpoints/screener.py,sha256=MYkIIZ6ocyx9khkxYjJnMQeg098-uZpdoSVbkUUj8Z4,9751
24
+ finbrain/aio/endpoints/screener.py,sha256=bmBs0uTrt7B8byaGfLTg8IfZckE_5GpeoUty79bea4Q,10272
24
25
  finbrain/aio/endpoints/senate_trades.py,sha256=nhZd8R7Tclxzn-J4-ZWZ5onvBBd8pVOtiLfhKFtFBHw,1431
25
26
  finbrain/aio/endpoints/sentiments.py,sha256=BXFT0jr5j0MnB65MXGxZK36BJLXMTuggkFF9A9eenEQ,1532
26
27
  finbrain/endpoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- finbrain/endpoints/_utils.py,sha256=0JIybEiSXVCm5SQI73i80OtoGQ2mJWAjImgA_2ru30U,896
28
+ finbrain/endpoints/_utils.py,sha256=wY8SKthpVernWlKwZfWoM8zZtQ3GS_hBQioH1Fk7RK4,1190
28
29
  finbrain/endpoints/analyst_ratings.py,sha256=6q_tecgnbzXn2wLSWqHNUa9MbLcybTD7xEBAkUyGPR0,2218
29
30
  finbrain/endpoints/app_ratings.py,sha256=mbeKiJ87nNibnz9AT2kmH4fVFA4goEcld910abm3tFs,3597
30
31
  finbrain/endpoints/available.py,sha256=3cXjDK1qE38RHAfQ2C14oD7l7dR8mhDtUcva3LzlK5s,3911
@@ -35,14 +36,15 @@ finbrain/endpoints/insider_transactions.py,sha256=TU6EsSHIeams4OLJ9s8BpNuXeBnWPH
35
36
  finbrain/endpoints/linkedin_data.py,sha256=zcNboQ-Ejt_7Ncu3p9WCd-PsjEBFbojVaJZdkbaGT8Q,2215
36
37
  finbrain/endpoints/news.py,sha256=q4aaEiunrUbl1FF0JZuX9__cZWxm2r1-B9x-c8g4Ia8,1913
37
38
  finbrain/endpoints/options.py,sha256=_TYWPcJ-gmPTQRg_kEVQl6Oj6fAirtZJ8QrfqFA2cMU,2611
39
+ finbrain/endpoints/patent_filings.py,sha256=496m-3lnGc3twMAdcIDOyMWs3KwHD2W8c_vzOl7rI3I,2362
38
40
  finbrain/endpoints/predictions.py,sha256=og1RY68p4O71smogrx-M5FyGk8FVclBGP18BvttzbZk,2026
39
41
  finbrain/endpoints/recent.py,sha256=7D36oVRJKF5hkR42y6M6CiEMYksuFleZou1v86SbEfA,3004
40
42
  finbrain/endpoints/reddit_mentions.py,sha256=WiePVAyUbA0VpWOd22M2YTlDWOQITp8qUBraI29y6d4,2305
41
- finbrain/endpoints/screener.py,sha256=jBPUvmfj0Zfj5aoDLBtfXlYIR0UynIG613duSkck3NY,9665
43
+ finbrain/endpoints/screener.py,sha256=Ebw4Oksi8JNj1B8xX2Mh2U7MqO57telCpxx0rphPCe4,10166
42
44
  finbrain/endpoints/senate_trades.py,sha256=K6r_-Wt1HOs0dDXngHdj6Eo_UUJXAy5yE4EmmQbGaiU,2226
43
45
  finbrain/endpoints/sentiments.py,sha256=sGGPaxzGGFvMOaYv3VSCmnG7H9v7OVlX7Vsndct8jhA,2606
44
- finbrain_python-0.2.4.dist-info/licenses/LICENSE,sha256=x71LjIUPbK7Y8YBulH_AXzEIX7BK90dgFL2zA6LBT-A,1069
45
- finbrain_python-0.2.4.dist-info/METADATA,sha256=qLUp9VxSkq5FRsYFAgpI5rLxClH-uUfcy2auc8bNLno,15402
46
- finbrain_python-0.2.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
47
- finbrain_python-0.2.4.dist-info/top_level.txt,sha256=dCiQtTx3z2TtO_yQI6q3yODcdb-pABB-iEReJegVAv0,9
48
- finbrain_python-0.2.4.dist-info/RECORD,,
46
+ finbrain_python-0.2.5.dist-info/licenses/LICENSE,sha256=x71LjIUPbK7Y8YBulH_AXzEIX7BK90dgFL2zA6LBT-A,1069
47
+ finbrain_python-0.2.5.dist-info/METADATA,sha256=Oxu3uLLP90SHboG4mMl2fQoJLDsqUTgGgu5XhMqhebs,16763
48
+ finbrain_python-0.2.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
49
+ finbrain_python-0.2.5.dist-info/top_level.txt,sha256=dCiQtTx3z2TtO_yQI6q3yODcdb-pABB-iEReJegVAv0,9
50
+ finbrain_python-0.2.5.dist-info/RECORD,,