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.
- sentisense-0.1.0/.gitignore +207 -0
- sentisense-0.1.0/LICENSE +21 -0
- sentisense-0.1.0/PKG-INFO +125 -0
- sentisense-0.1.0/README.md +96 -0
- sentisense-0.1.0/pyproject.toml +45 -0
- sentisense-0.1.0/src/sentisense/__about__.py +1 -0
- sentisense-0.1.0/src/sentisense/__init__.py +21 -0
- sentisense-0.1.0/src/sentisense/client.py +166 -0
- sentisense-0.1.0/src/sentisense/exceptions.py +60 -0
- sentisense-0.1.0/src/sentisense/py.typed +0 -0
- sentisense-0.1.0/tests/__init__.py +0 -0
- sentisense-0.1.0/tests/test_client.py +173 -0
|
@@ -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__/
|
sentisense-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/sentisense/)
|
|
33
|
+
[](https://pypi.org/project/sentisense/)
|
|
34
|
+
[](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
|
+
[](https://pypi.org/project/sentisense/)
|
|
4
|
+
[](https://pypi.org/project/sentisense/)
|
|
5
|
+
[](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
|