sentisense 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SentiSenseApp Official
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: sentisense
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the SentiSense market intelligence API
5
+ Project-URL: Homepage, https://sentisense.ai
6
+ Project-URL: Documentation, https://github.com/SentiSenseApp/sentisense#readme
7
+ Project-URL: Repository, https://github.com/SentiSenseApp/sentisense
8
+ Project-URL: Issues, https://github.com/SentiSenseApp/sentisense/issues
9
+ Author-email: SentiSense <support@sentisense.ai>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,finance,market,sdk,sentiment,sentisense,stocks
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Office/Business :: Financial :: Investment
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.8
27
+ Requires-Dist: requests>=2.20.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # SentiSense Python SDK
31
+
32
+ [![PyPI version](https://img.shields.io/pypi/v/sentisense.svg)](https://pypi.org/project/sentisense/)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/sentisense.svg)](https://pypi.org/project/sentisense/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
35
+
36
+ Official Python SDK for the [SentiSense](https://sentisense.ai) market intelligence API.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install sentisense
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ from sentisense import SentiSenseClient
48
+
49
+ client = SentiSenseClient("your-api-key")
50
+
51
+ # Get a stock price
52
+ price = client.get_stock_price("AAPL")
53
+ print(price)
54
+
55
+ # Get multiple stock prices
56
+ prices = client.get_stock_prices(["AAPL", "MSFT", "GOOGL"])
57
+
58
+ # Check market status
59
+ status = client.get_market_status()
60
+ print(status)
61
+ ```
62
+
63
+ ## Authentication
64
+
65
+ All API requests require an API key. You can generate one from your [Developer Console](https://app.sentisense.ai/settings/developer).
66
+
67
+ ```python
68
+ client = SentiSenseClient("your-api-key")
69
+ ```
70
+
71
+ For full endpoint documentation, request/response schemas, and interactive examples, see the [API Documentation](https://sentisense.ai/docs/api/).
72
+
73
+ ## API Reference
74
+
75
+ ### Stocks
76
+
77
+ | Method | Description |
78
+ |--------|-------------|
79
+ | `get_stock_price(ticker)` | Real-time price for a single stock |
80
+ | `get_stock_prices(tickers)` | Real-time prices for multiple stocks |
81
+ | `get_stock_profile(ticker)` | Company profile |
82
+ | `get_stock_chart(ticker, timeframe="1M")` | OHLCV chart data |
83
+ | `get_all_stocks()` | List of available tickers |
84
+ | `get_all_stocks_detailed()` | Tickers with company names and entity IDs |
85
+ | `get_market_status()` | Market open/closed status |
86
+ | `get_fundamentals(ticker, timeframe="quarterly")` | Financial fundamentals |
87
+
88
+ ### Institutional Flows (13F)
89
+
90
+ | Method | Description |
91
+ |--------|-------------|
92
+ | `get_institutional_quarters()` | Available 13F reporting quarters |
93
+ | `get_institutional_flows(report_date, limit=50)` | Fund flows for a quarter |
94
+ | `get_stock_holders(ticker, report_date)` | Institutional holders for a stock |
95
+ | `get_activist_positions(report_date)` | Activist investor positions |
96
+
97
+ ## Error Handling
98
+
99
+ The SDK raises typed exceptions for API errors:
100
+
101
+ ```python
102
+ from sentisense import SentiSenseClient, AuthenticationError, RateLimitError
103
+
104
+ client = SentiSenseClient("your-api-key")
105
+
106
+ try:
107
+ price = client.get_stock_price("AAPL")
108
+ except AuthenticationError:
109
+ print("Invalid or missing API key")
110
+ except RateLimitError:
111
+ print("Rate limit exceeded, try again later")
112
+ ```
113
+
114
+ | Exception | HTTP Status | Description |
115
+ |-----------|-------------|-------------|
116
+ | `AuthenticationError` | 401, 403 | Invalid or missing API key |
117
+ | `NotFoundError` | 404 | Resource not found |
118
+ | `RateLimitError` | 429 | Rate limit exceeded |
119
+ | `APIError` | Other 4xx/5xx | General API error |
120
+
121
+ All exceptions inherit from `SentiSenseError` and include `.status_code`, `.message`, and `.response` attributes.
122
+
123
+ ## License
124
+
125
+ MIT - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,96 @@
1
+ # SentiSense Python SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/sentisense.svg)](https://pypi.org/project/sentisense/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/sentisense.svg)](https://pypi.org/project/sentisense/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Official Python SDK for the [SentiSense](https://sentisense.ai) market intelligence API.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install sentisense
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ from sentisense import SentiSenseClient
19
+
20
+ client = SentiSenseClient("your-api-key")
21
+
22
+ # Get a stock price
23
+ price = client.get_stock_price("AAPL")
24
+ print(price)
25
+
26
+ # Get multiple stock prices
27
+ prices = client.get_stock_prices(["AAPL", "MSFT", "GOOGL"])
28
+
29
+ # Check market status
30
+ status = client.get_market_status()
31
+ print(status)
32
+ ```
33
+
34
+ ## Authentication
35
+
36
+ All API requests require an API key. You can generate one from your [Developer Console](https://app.sentisense.ai/settings/developer).
37
+
38
+ ```python
39
+ client = SentiSenseClient("your-api-key")
40
+ ```
41
+
42
+ For full endpoint documentation, request/response schemas, and interactive examples, see the [API Documentation](https://sentisense.ai/docs/api/).
43
+
44
+ ## API Reference
45
+
46
+ ### Stocks
47
+
48
+ | Method | Description |
49
+ |--------|-------------|
50
+ | `get_stock_price(ticker)` | Real-time price for a single stock |
51
+ | `get_stock_prices(tickers)` | Real-time prices for multiple stocks |
52
+ | `get_stock_profile(ticker)` | Company profile |
53
+ | `get_stock_chart(ticker, timeframe="1M")` | OHLCV chart data |
54
+ | `get_all_stocks()` | List of available tickers |
55
+ | `get_all_stocks_detailed()` | Tickers with company names and entity IDs |
56
+ | `get_market_status()` | Market open/closed status |
57
+ | `get_fundamentals(ticker, timeframe="quarterly")` | Financial fundamentals |
58
+
59
+ ### Institutional Flows (13F)
60
+
61
+ | Method | Description |
62
+ |--------|-------------|
63
+ | `get_institutional_quarters()` | Available 13F reporting quarters |
64
+ | `get_institutional_flows(report_date, limit=50)` | Fund flows for a quarter |
65
+ | `get_stock_holders(ticker, report_date)` | Institutional holders for a stock |
66
+ | `get_activist_positions(report_date)` | Activist investor positions |
67
+
68
+ ## Error Handling
69
+
70
+ The SDK raises typed exceptions for API errors:
71
+
72
+ ```python
73
+ from sentisense import SentiSenseClient, AuthenticationError, RateLimitError
74
+
75
+ client = SentiSenseClient("your-api-key")
76
+
77
+ try:
78
+ price = client.get_stock_price("AAPL")
79
+ except AuthenticationError:
80
+ print("Invalid or missing API key")
81
+ except RateLimitError:
82
+ print("Rate limit exceeded, try again later")
83
+ ```
84
+
85
+ | Exception | HTTP Status | Description |
86
+ |-----------|-------------|-------------|
87
+ | `AuthenticationError` | 401, 403 | Invalid or missing API key |
88
+ | `NotFoundError` | 404 | Resource not found |
89
+ | `RateLimitError` | 429 | Rate limit exceeded |
90
+ | `APIError` | Other 4xx/5xx | General API error |
91
+
92
+ All exceptions inherit from `SentiSenseError` and include `.status_code`, `.message`, and `.response` attributes.
93
+
94
+ ## License
95
+
96
+ MIT - see [LICENSE](LICENSE) for details.
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sentisense"
7
+ dynamic = ["version"]
8
+ description = "Official Python SDK for the SentiSense market intelligence API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "SentiSense", email = "support@sentisense.ai" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Financial and Insurance Industry",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Office/Business :: Financial :: Investment",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = [
31
+ "requests>=2.20.0",
32
+ ]
33
+ keywords = ["sentisense", "finance", "stocks", "market", "sentiment", "api", "sdk"]
34
+
35
+ [project.urls]
36
+ Homepage = "https://sentisense.ai"
37
+ Documentation = "https://github.com/SentiSenseApp/sentisense#readme"
38
+ Repository = "https://github.com/SentiSenseApp/sentisense"
39
+ Issues = "https://github.com/SentiSenseApp/sentisense/issues"
40
+
41
+ [tool.hatch.version]
42
+ path = "src/sentisense/__about__.py"
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,21 @@
1
+ """SentiSense — Official Python SDK for the SentiSense market intelligence API."""
2
+
3
+ from sentisense.__about__ import __version__
4
+ from sentisense.client import SentiSenseClient
5
+ from sentisense.exceptions import (
6
+ APIError,
7
+ AuthenticationError,
8
+ NotFoundError,
9
+ RateLimitError,
10
+ SentiSenseError,
11
+ )
12
+
13
+ __all__ = [
14
+ "__version__",
15
+ "APIError",
16
+ "AuthenticationError",
17
+ "NotFoundError",
18
+ "RateLimitError",
19
+ "SentiSenseClient",
20
+ "SentiSenseError",
21
+ ]
@@ -0,0 +1,166 @@
1
+ """SentiSense API client."""
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import requests
6
+
7
+ from sentisense.__about__ import __version__
8
+ from sentisense.exceptions import _raise_for_status
9
+
10
+
11
+ class SentiSenseClient:
12
+ """Official Python client for the SentiSense market intelligence API.
13
+
14
+ Usage::
15
+
16
+ from sentisense import SentiSenseClient
17
+
18
+ client = SentiSenseClient("your-api-key")
19
+ price = client.get_stock_price("AAPL")
20
+ """
21
+
22
+ BASE_URL = "https://app.sentisense.ai"
23
+
24
+ def __init__(
25
+ self,
26
+ api_key: str,
27
+ *,
28
+ base_url: str = BASE_URL,
29
+ timeout: float = 30.0,
30
+ ):
31
+ self.base_url = base_url.rstrip("/")
32
+ self.timeout = timeout
33
+ self.session = requests.Session()
34
+ self.session.headers.update({
35
+ "X-SentiSense-API-Key": api_key,
36
+ "User-Agent": f"sentisense-python/{__version__}",
37
+ })
38
+
39
+ # ── Private HTTP helpers ────────────────────────────────────
40
+
41
+ def _url(self, path: str) -> str:
42
+ if path.startswith("http"):
43
+ return path
44
+ return f"{self.base_url}{path}"
45
+
46
+ def _get(self, path: str, **kwargs: Any) -> requests.Response:
47
+ kwargs.setdefault("timeout", self.timeout)
48
+ resp = self.session.get(self._url(path), **kwargs)
49
+ _raise_for_status(resp)
50
+ return resp
51
+
52
+ def _post(self, path: str, **kwargs: Any) -> requests.Response:
53
+ kwargs.setdefault("timeout", self.timeout)
54
+ resp = self.session.post(self._url(path), **kwargs)
55
+ _raise_for_status(resp)
56
+ return resp
57
+
58
+ def _put(self, path: str, **kwargs: Any) -> requests.Response:
59
+ kwargs.setdefault("timeout", self.timeout)
60
+ resp = self.session.put(self._url(path), **kwargs)
61
+ _raise_for_status(resp)
62
+ return resp
63
+
64
+ def _delete(self, path: str, **kwargs: Any) -> requests.Response:
65
+ kwargs.setdefault("timeout", self.timeout)
66
+ resp = self.session.delete(self._url(path), **kwargs)
67
+ _raise_for_status(resp)
68
+ return resp
69
+
70
+ # ── Stock endpoints ─────────────────────────────────────────
71
+
72
+ def get_stock_price(self, ticker: str) -> Dict[str, Any]:
73
+ """Get real-time stock price for a single ticker."""
74
+ return self._get("/api/v1/stocks/price", params={"ticker": ticker}).json()
75
+
76
+ def get_stock_prices(self, tickers: List[str]) -> List[Dict[str, Any]]:
77
+ """Get real-time stock prices for multiple tickers."""
78
+ return self._get("/api/v1/stocks/prices", params={"tickers": ",".join(tickers)}).json()
79
+
80
+ def get_stock_profile(self, ticker: str) -> Dict[str, Any]:
81
+ """Get company profile for a stock."""
82
+ return self._get(f"/api/v1/stocks/{ticker}/profile").json()
83
+
84
+ def get_stock_chart(self, ticker: str, timeframe: str = "1M") -> Dict[str, Any]:
85
+ """Get OHLCV chart data for a stock.
86
+
87
+ Args:
88
+ ticker: Stock ticker symbol.
89
+ timeframe: Chart timeframe (e.g. "1D", "1W", "1M", "3M", "1Y").
90
+ """
91
+ return self._get("/api/v1/stocks/chart", params={"ticker": ticker, "timeframe": timeframe}).json()
92
+
93
+ def get_all_stocks(self) -> List[str]:
94
+ """Get all available stock tickers."""
95
+ return self._get("/api/v1/stocks").json()
96
+
97
+ def get_all_stocks_detailed(self) -> List[Dict[str, Any]]:
98
+ """Get all stocks with company names and entity IDs."""
99
+ return self._get("/api/v1/stocks/detailed").json()
100
+
101
+ def get_market_status(self) -> Dict[str, Any]:
102
+ """Get current market status (open/closed)."""
103
+ return self._get("/api/v1/stocks/market-status").json()
104
+
105
+ def get_fundamentals(
106
+ self,
107
+ ticker: str,
108
+ timeframe: str = "quarterly",
109
+ fiscal_period: Optional[str] = None,
110
+ fiscal_year: Optional[int] = None,
111
+ ) -> Dict[str, Any]:
112
+ """Get fundamental financial data for a stock.
113
+
114
+ Args:
115
+ ticker: Stock ticker symbol.
116
+ timeframe: "quarterly" or "annual".
117
+ fiscal_period: Filter by fiscal period (e.g. "Q1", "Q2").
118
+ fiscal_year: Filter by fiscal year (e.g. 2025).
119
+ """
120
+ params: Dict[str, Any] = {"ticker": ticker, "timeframe": timeframe}
121
+ if fiscal_period:
122
+ params["fiscalPeriod"] = fiscal_period
123
+ if fiscal_year:
124
+ params["fiscalYear"] = fiscal_year
125
+ return self._get("/api/v1/stocks/fundamentals", params=params).json()
126
+
127
+ # ── Institutional flow endpoints ────────────────────────────
128
+
129
+ def get_institutional_quarters(self) -> List[str]:
130
+ """Get available 13F reporting quarters."""
131
+ return self._get("/api/v1/institutional/quarters").json()
132
+
133
+ def get_institutional_flows(self, report_date: str, limit: int = 50) -> List[Dict[str, Any]]:
134
+ """Get institutional fund flows for a reporting quarter.
135
+
136
+ Args:
137
+ report_date: Quarter date string (e.g. "2025-03-31").
138
+ limit: Maximum number of results.
139
+ """
140
+ return self._get(
141
+ "/api/v1/institutional/flows",
142
+ params={"reportDate": report_date, "limit": limit},
143
+ ).json()
144
+
145
+ def get_stock_holders(self, ticker: str, report_date: str) -> List[Dict[str, Any]]:
146
+ """Get institutional holders for a specific stock.
147
+
148
+ Args:
149
+ ticker: Stock ticker symbol.
150
+ report_date: Quarter date string (e.g. "2025-03-31").
151
+ """
152
+ return self._get(
153
+ f"/api/v1/institutional/holders/{ticker}",
154
+ params={"reportDate": report_date},
155
+ ).json()
156
+
157
+ def get_activist_positions(self, report_date: str) -> List[Dict[str, Any]]:
158
+ """Get activist investor positions for a reporting quarter.
159
+
160
+ Args:
161
+ report_date: Quarter date string (e.g. "2025-03-31").
162
+ """
163
+ return self._get(
164
+ "/api/v1/institutional/activist",
165
+ params={"reportDate": report_date},
166
+ ).json()
@@ -0,0 +1,60 @@
1
+ """SentiSense API exceptions."""
2
+
3
+ from typing import Optional
4
+
5
+ import requests
6
+
7
+
8
+ class SentiSenseError(Exception):
9
+ """Base exception for all SentiSense SDK errors."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ status_code: Optional[int] = None,
15
+ response: Optional[requests.Response] = None,
16
+ ):
17
+ super().__init__(message)
18
+ self.message = message
19
+ self.status_code = status_code
20
+ self.response = response
21
+
22
+
23
+ class AuthenticationError(SentiSenseError):
24
+ """Raised on 401 or 403 responses (invalid or missing API key)."""
25
+
26
+
27
+ class NotFoundError(SentiSenseError):
28
+ """Raised on 404 responses."""
29
+
30
+
31
+ class RateLimitError(SentiSenseError):
32
+ """Raised on 429 responses (rate limit exceeded)."""
33
+
34
+
35
+ class APIError(SentiSenseError):
36
+ """Raised on other non-2xx responses."""
37
+
38
+
39
+ def _raise_for_status(response: requests.Response) -> None:
40
+ """Raise a typed SentiSenseError for non-2xx responses."""
41
+ if response.ok:
42
+ return
43
+
44
+ try:
45
+ body = response.json()
46
+ message = body.get("message") or body.get("error") or response.reason
47
+ except (ValueError, KeyError):
48
+ message = response.reason or f"HTTP {response.status_code}"
49
+
50
+ status = response.status_code
51
+ kwargs = dict(message=message, status_code=status, response=response)
52
+
53
+ if status in (401, 403):
54
+ raise AuthenticationError(**kwargs)
55
+ elif status == 404:
56
+ raise NotFoundError(**kwargs)
57
+ elif status == 429:
58
+ raise RateLimitError(**kwargs)
59
+ else:
60
+ raise APIError(**kwargs)
File without changes
File without changes
@@ -0,0 +1,173 @@
1
+ """Unit tests for SentiSenseClient."""
2
+
3
+ import json
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from sentisense import (
9
+ SentiSenseClient,
10
+ AuthenticationError,
11
+ NotFoundError,
12
+ RateLimitError,
13
+ APIError,
14
+ __version__,
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def client():
20
+ return SentiSenseClient("test-api-key")
21
+
22
+
23
+ def _mock_response(status_code=200, json_data=None, reason="OK"):
24
+ resp = MagicMock()
25
+ resp.status_code = status_code
26
+ resp.ok = 200 <= status_code < 300
27
+ resp.reason = reason
28
+ resp.json.return_value = json_data or {}
29
+ return resp
30
+
31
+
32
+ class TestClientConstruction:
33
+ def test_default_base_url(self, client):
34
+ assert client.base_url == "https://app.sentisense.ai"
35
+
36
+ def test_custom_base_url(self):
37
+ c = SentiSenseClient("key", base_url="https://custom.example.com/")
38
+ assert c.base_url == "https://custom.example.com"
39
+
40
+ def test_api_key_header(self, client):
41
+ assert client.session.headers["X-SentiSense-API-Key"] == "test-api-key"
42
+
43
+ def test_user_agent_header(self, client):
44
+ assert client.session.headers["User-Agent"] == f"sentisense-python/{__version__}"
45
+
46
+ def test_default_timeout(self, client):
47
+ assert client.timeout == 30.0
48
+
49
+ def test_custom_timeout(self):
50
+ c = SentiSenseClient("key", timeout=10.0)
51
+ assert c.timeout == 10.0
52
+
53
+
54
+ class TestStockEndpoints:
55
+ @patch.object(SentiSenseClient, "_get")
56
+ def test_get_stock_price(self, mock_get, client):
57
+ mock_get.return_value = _mock_response(json_data={"price": 150.0})
58
+ result = client.get_stock_price("AAPL")
59
+ mock_get.assert_called_once_with("/api/v1/stocks/price", params={"ticker": "AAPL"})
60
+ assert result == {"price": 150.0}
61
+
62
+ @patch.object(SentiSenseClient, "_get")
63
+ def test_get_stock_prices(self, mock_get, client):
64
+ mock_get.return_value = _mock_response(json_data=[{"ticker": "AAPL"}, {"ticker": "MSFT"}])
65
+ result = client.get_stock_prices(["AAPL", "MSFT"])
66
+ mock_get.assert_called_once_with("/api/v1/stocks/prices", params={"tickers": "AAPL,MSFT"})
67
+ assert len(result) == 2
68
+
69
+ @patch.object(SentiSenseClient, "_get")
70
+ def test_get_stock_profile(self, mock_get, client):
71
+ mock_get.return_value = _mock_response(json_data={"name": "Apple Inc."})
72
+ result = client.get_stock_profile("AAPL")
73
+ mock_get.assert_called_once_with("/api/v1/stocks/AAPL/profile")
74
+ assert result["name"] == "Apple Inc."
75
+
76
+ @patch.object(SentiSenseClient, "_get")
77
+ def test_get_stock_chart(self, mock_get, client):
78
+ mock_get.return_value = _mock_response(json_data={"candles": []})
79
+ client.get_stock_chart("AAPL", timeframe="1W")
80
+ mock_get.assert_called_once_with(
81
+ "/api/v1/stocks/chart", params={"ticker": "AAPL", "timeframe": "1W"}
82
+ )
83
+
84
+ @patch.object(SentiSenseClient, "_get")
85
+ def test_get_all_stocks(self, mock_get, client):
86
+ mock_get.return_value = _mock_response(json_data=["AAPL", "MSFT"])
87
+ result = client.get_all_stocks()
88
+ mock_get.assert_called_once_with("/api/v1/stocks")
89
+ assert result == ["AAPL", "MSFT"]
90
+
91
+ @patch.object(SentiSenseClient, "_get")
92
+ def test_get_market_status(self, mock_get, client):
93
+ mock_get.return_value = _mock_response(json_data={"status": "open"})
94
+ result = client.get_market_status()
95
+ mock_get.assert_called_once_with("/api/v1/stocks/market-status")
96
+ assert result["status"] == "open"
97
+
98
+ @patch.object(SentiSenseClient, "_get")
99
+ def test_get_fundamentals_with_filters(self, mock_get, client):
100
+ mock_get.return_value = _mock_response(json_data={})
101
+ client.get_fundamentals("AAPL", timeframe="annual", fiscal_period="Q1", fiscal_year=2025)
102
+ mock_get.assert_called_once_with(
103
+ "/api/v1/stocks/fundamentals",
104
+ params={"ticker": "AAPL", "timeframe": "annual", "fiscalPeriod": "Q1", "fiscalYear": 2025},
105
+ )
106
+
107
+
108
+ class TestInstitutionalEndpoints:
109
+ @patch.object(SentiSenseClient, "_get")
110
+ def test_get_institutional_quarters(self, mock_get, client):
111
+ mock_get.return_value = _mock_response(json_data=["2025-03-31"])
112
+ result = client.get_institutional_quarters()
113
+ mock_get.assert_called_once_with("/api/v1/institutional/quarters")
114
+ assert result == ["2025-03-31"]
115
+
116
+ @patch.object(SentiSenseClient, "_get")
117
+ def test_get_institutional_flows(self, mock_get, client):
118
+ mock_get.return_value = _mock_response(json_data=[])
119
+ client.get_institutional_flows("2025-03-31", limit=10)
120
+ mock_get.assert_called_once_with(
121
+ "/api/v1/institutional/flows",
122
+ params={"reportDate": "2025-03-31", "limit": 10},
123
+ )
124
+
125
+ @patch.object(SentiSenseClient, "_get")
126
+ def test_get_stock_holders(self, mock_get, client):
127
+ mock_get.return_value = _mock_response(json_data=[])
128
+ client.get_stock_holders("AAPL", "2025-03-31")
129
+ mock_get.assert_called_once_with(
130
+ "/api/v1/institutional/holders/AAPL",
131
+ params={"reportDate": "2025-03-31"},
132
+ )
133
+
134
+ @patch.object(SentiSenseClient, "_get")
135
+ def test_get_activist_positions(self, mock_get, client):
136
+ mock_get.return_value = _mock_response(json_data=[])
137
+ client.get_activist_positions("2025-03-31")
138
+ mock_get.assert_called_once_with(
139
+ "/api/v1/institutional/activist",
140
+ params={"reportDate": "2025-03-31"},
141
+ )
142
+
143
+
144
+ class TestErrorHandling:
145
+ def test_401_raises_authentication_error(self, client):
146
+ with patch.object(client.session, "get", return_value=_mock_response(401, {"message": "Invalid API key"}, "Unauthorized")):
147
+ with pytest.raises(AuthenticationError) as exc_info:
148
+ client.get_stock_price("AAPL")
149
+ assert exc_info.value.status_code == 401
150
+ assert "Invalid API key" in exc_info.value.message
151
+
152
+ def test_403_raises_authentication_error(self, client):
153
+ with patch.object(client.session, "get", return_value=_mock_response(403, {}, "Forbidden")):
154
+ with pytest.raises(AuthenticationError) as exc_info:
155
+ client.get_stock_price("AAPL")
156
+ assert exc_info.value.status_code == 403
157
+
158
+ def test_404_raises_not_found_error(self, client):
159
+ with patch.object(client.session, "get", return_value=_mock_response(404, {}, "Not Found")):
160
+ with pytest.raises(NotFoundError):
161
+ client.get_stock_profile("INVALID")
162
+
163
+ def test_429_raises_rate_limit_error(self, client):
164
+ with patch.object(client.session, "get", return_value=_mock_response(429, {"message": "Rate limit exceeded"}, "Too Many Requests")):
165
+ with pytest.raises(RateLimitError) as exc_info:
166
+ client.get_all_stocks()
167
+ assert "Rate limit exceeded" in exc_info.value.message
168
+
169
+ def test_500_raises_api_error(self, client):
170
+ with patch.object(client.session, "get", return_value=_mock_response(500, {}, "Internal Server Error")):
171
+ with pytest.raises(APIError) as exc_info:
172
+ client.get_market_status()
173
+ assert exc_info.value.status_code == 500