Skip to content

Commit cb90e63

Browse files
committed
fix(sdk): align python client with current currents api and modernize packaging
1 parent 9ec175a commit cb90e63

11 files changed

Lines changed: 393 additions & 354 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
pull_request:
7+
branches: [master, main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Set up Python ${{ matrix.python-version }}
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -e .
26+
pip install pytest
27+
- name: Run tests
28+
run: pytest tests/

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,4 @@ venv.bak/
121121
dmypy.json
122122

123123
# Pyre type checker
124-
.pyre/
124+
.pyre/tests/integration_test.py

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Changelog
2+
3+
## 0.1.0
4+
5+
### Added
6+
- Added `available_languages()`, `available_regions()`, and `available_category()` endpoint wrappers.
7+
- Added `language` parameter support to `latest_news()`.
8+
- Added GitHub Actions CI workflow.
9+
- Added unit tests using `unittest.mock`.
10+
11+
### Changed
12+
- Aligned `search()` parameters with the current documented API surface.
13+
- Modernized packaging metadata: updated repository URL, classifiers, and dependency pins.
14+
- Unified package version to `0.1.0` across `setup.py` and `currentsapi/__init__.py`.
15+
- Updated README with current docs link and usage examples.
16+
17+
### Fixed
18+
- Fixed `start_date` handling in `search()` which incorrectly referenced `end_date`.
19+
- Renamed exception class to `CurrentsAPIError` with cleaner attribute access.
20+
21+
### Removed
22+
- Removed unsupported `search()` parameters: `page_number`, `limit`, `has_image`, `has_description`.
23+
- Removed the "subtly broken or buggy" disclaimer from the README.

README.md

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,72 @@
11
# currentsapi-python
2-
A Python client for the [Currents API](https://currentsapi.services/documents)
32

4-
##### Provided under MIT License by Zhi Rui Tam.
5-
*Note: this library may be subtly broken or buggy. The code is released under
6-
the MIT License – please take the following message to heart:*
7-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
8-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
9-
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
10-
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
11-
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
12-
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13-
14-
## General
15-
16-
This is a Python client library for CurrentsAPI version 1. The functions for the library should mirror the
17-
endpoints from the [documentation](https://currentsapi.services/documents).
3+
The official Python SDK for the [Currents API](https://currentsapi.services/en/docs/).
184

195
## Installation
20-
Installation for the package can be done via pip.
216

22-
```commandline
23-
pip install currentsapi-python
7+
Install the package from PyPI:
8+
9+
```bash
10+
pip install currentsapi
2411
```
2512

2613
## Usage
2714

28-
After installation, import client into your project:
15+
Import the client and initialize it with your API key:
2916

3017
```python
3118
from currentsapi import CurrentsAPI
19+
20+
api = CurrentsAPI(api_key="YOUR_API_KEY")
3221
```
3322

34-
Initialize the client with your API key:
23+
## Endpoints
24+
25+
### Latest News
26+
27+
Retrieve the latest news headlines. Optionally filter by language:
3528

3629
```python
37-
api = CurrentsAPI(api_key='XXXXXXXXXXXXXXXXXXXXXXX')
30+
api.latest_news()
31+
api.latest_news(language="en")
3832
```
3933

40-
### Endpoints
41-
42-
#### Latest News
34+
### Search
35+
36+
Search news articles with optional filters:
4337

4438
```python
45-
api.latest_news()
39+
api.search(keywords="OpenAI", language="en")
40+
api.search(country="US", category="technology", start_date="2024-01-01", end_date="2024-12-31")
4641
```
47-
#### Query
42+
43+
Supported parameters:
44+
45+
- `keywords` – search keywords
46+
- `language` – article language code
47+
- `country` – country code
48+
- `category` – news category
49+
- `start_date` – start date (`YYYY-MM-DD` or `datetime` object)
50+
- `end_date` – end date (`YYYY-MM-DD` or `datetime` object)
51+
52+
### Available Resources
4853

4954
```python
50-
api.search(keywords='Trump')
55+
api.available_languages()
56+
api.available_regions()
57+
api.available_category()
5158
```
5259

60+
## Authentication
61+
62+
All requests are authenticated using an `Authorization` header. Pass your API key when instantiating the client:
63+
64+
```python
65+
api = CurrentsAPI(api_key="YOUR_API_KEY")
66+
```
67+
68+
Get your API key at [https://currentsapi.services/en/register](https://currentsapi.services/en/register).
69+
70+
## License
71+
72+
MIT License

currentsapi/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import sys
22

33
__project__ = "currentsapi"
4-
__version__ = "0.0.2"
5-
__repo__ = ""
4+
__version__ = "0.1.0"
5+
__repo__ = "https://github.com/currentslab/currentsapi-python"
66

77
from currentsapi.client import CurrentsAPI
88

9+
910
def print_version():
1011
sv = sys.version_info
1112
py_version = "{}.{}.{}".format(sv.major, sv.minor, sv.micro)
@@ -15,4 +16,4 @@ def print_version():
1516
s += "\nMinor version: {} (extra feature)".format(version_parts[1])
1617
s += "\nMicro version: {} (commit count)".format(version_parts[2])
1718
s += "\nFind out the most recent version at {}".format(__repo__)
18-
return s
19+
return s

currentsapi/client.py

Lines changed: 92 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,127 @@
1-
import requests
21
import datetime
2+
import requests
33
from dateutil import parser
4-
from currentsapi.authentication import ApiAuth
5-
from currentsapi import constants
64

7-
class APIException(Exception):
5+
from currentsapi import constants
6+
from currentsapi.authentication import ApiAuth
87

9-
def __init__(self, exception):
10-
self.exception = exception
118

12-
def get_exception(self):
13-
return self.exception
9+
class CurrentsAPIError(Exception):
10+
"""Raised when the Currents API returns an error response."""
1411

15-
def get_status(self):
16-
if self.exception["status"]:
17-
return self.exception["status"]
12+
def __init__(self, response):
13+
self.response = response
14+
super().__init__(str(response))
1815

19-
def get_code(self):
20-
if self.exception["code"]:
21-
return self.exception["code"]
16+
@property
17+
def status(self):
18+
return self.response.get("status")
2219

23-
def get_message(self):
24-
if self.exception["message"]:
25-
return self.exception["message"]
20+
@property
21+
def code(self):
22+
return self.response.get("code")
2623

24+
@property
25+
def message(self):
26+
return self.response.get("message")
2727

28-
class CurrentsAPI():
2928

30-
def __init__(self, api_key,
31-
domain=constants.DOMAIN, version=constants.VERSION, timeout=30):
29+
class CurrentsAPI:
30+
def __init__(
31+
self,
32+
api_key,
33+
domain=constants.DOMAIN,
34+
version=constants.VERSION,
35+
timeout=30,
36+
):
3237
if not isinstance(api_key, str):
33-
raise ValueError('api_key must be string')
38+
raise ValueError("api_key must be a string")
3439
self.api_key = ApiAuth(api_key)
3540
self.latest_endpoint = constants.LATEST_NEWS_URL % (domain, version)
3641
self.search_endpoint = constants.SEARCH_URL % (domain, version)
42+
self.available_languages_endpoint = constants.AVAILABLE_LANGUAGES_URL % (domain, version)
43+
self.available_regions_endpoint = constants.AVAILABLE_REGIONS_URL % (domain, version)
44+
self.available_category_endpoint = constants.AVAILABLE_CATEGORIES_URL % (domain, version)
3745
self.timeout = timeout
3846

39-
def latest_news(self):
40-
r = requests.get(self.latest_endpoint, auth=self.api_key, timeout=self.timeout)
47+
def _get(self, endpoint, params=None):
48+
r = requests.get(
49+
endpoint,
50+
auth=self.api_key,
51+
timeout=self.timeout,
52+
params=params or {},
53+
)
4154
if r.status_code != requests.codes.ok:
42-
raise APIException(r.json())
55+
raise CurrentsAPIError(r.json())
4356
return r.json()
44-
4557

46-
def search(self, country=None, language=None, keywords=None, category=None,
47-
page_number=None, limit=None, start_date=None, end_date=None, has_image=None, has_description=None):
48-
payload = {}
58+
def latest_news(self, language=None):
59+
params = {}
60+
if language:
61+
if not isinstance(language, str):
62+
raise ValueError("language must be a string")
63+
params["language"] = language
64+
return self._get(self.latest_endpoint, params)
65+
66+
def search(
67+
self,
68+
language=None,
69+
keywords=None,
70+
country=None,
71+
category=None,
72+
start_date=None,
73+
end_date=None,
74+
):
75+
params = {}
4976

5077
if keywords:
5178
if not isinstance(keywords, str):
52-
raise ValueError('keywords should be string')
53-
payload['keywords'] = keywords
54-
79+
raise ValueError("keywords must be a string")
80+
params["keywords"] = keywords
5581

5682
if country:
5783
if not isinstance(country, str):
58-
raise ValueError('country should be string')
59-
payload['country'] = country
84+
raise ValueError("country must be a string")
85+
params["country"] = country
6086

6187
if language:
6288
if not isinstance(language, str):
63-
raise ValueError('language should be string')
64-
payload['language'] = language
89+
raise ValueError("language must be a string")
90+
params["language"] = language
6591

6692
if category:
67-
if not isinstance(category, str) and not isinstance(category, list):
68-
raise ValueError('category should be string')
69-
if isinstance(category, list):
70-
payload['category'] = ','.join(category)
71-
else:
72-
payload['category'] = category
73-
74-
if page_number:
75-
if not int(page_number) == page_number:
76-
raise ValueError('page_number should be integer')
77-
payload['page_number'] = page_number
78-
79-
if limit:
80-
if not int(limit) == limit:
81-
raise ValueError('limit should be integer')
82-
payload['limit'] = limit
83-
93+
if not isinstance(category, str):
94+
raise ValueError("category must be a string")
95+
params["category"] = category
8496

8597
if start_date:
86-
if isinstance(start_date, str):
87-
date = parser(start_date)
88-
elif isinstance(end_date, datetime.date):
89-
date = end_date
90-
else:
91-
raise ValueError('start_date must be string parsable by dateutil or datetime object')
92-
payload['start_date'] = date.strftime('%Y-%m-%dT%H:%M:%SZ')
98+
date = self._parse_date(start_date, "start_date")
99+
params["start_date"] = date.strftime("%Y-%m-%dT%H:%M:%SZ")
93100

94101
if end_date:
95-
if isinstance(end_date, str):
96-
date = parser(end_date)
97-
elif isinstance(end_date, datetime.date):
98-
date = end_date
99-
else:
100-
raise ValueError('end_date must be string parsable by dateutil or datetime object')
101-
payload['end_date'] = date.strftime('%Y-%m-%dT%H:%M:%SZ')
102-
103-
if has_image:
104-
payload['has_image'] = 'true' if has_image else 'false'
105-
106-
if has_description:
107-
payload['has_description'] = 'true' if has_description else 'false'
108-
r = requests.get(self.search_endpoint, auth=self.api_key,
109-
timeout=self.timeout,
110-
params=payload)
111-
112-
if r.status_code != requests.codes.ok:
113-
raise APIException(r.json())
114-
115-
return r.json()
102+
date = self._parse_date(end_date, "end_date")
103+
params["end_date"] = date.strftime("%Y-%m-%dT%H:%M:%SZ")
104+
105+
return self._get(self.search_endpoint, params)
106+
107+
def available_languages(self):
108+
return self._get(self.available_languages_endpoint)
109+
110+
def available_regions(self):
111+
return self._get(self.available_regions_endpoint)
112+
113+
def available_category(self):
114+
return self._get(self.available_category_endpoint)
115+
116+
@staticmethod
117+
def _parse_date(date_value, param_name):
118+
if isinstance(date_value, str):
119+
return parser.parse(date_value)
120+
elif isinstance(date_value, datetime.date):
121+
return date_value
122+
else:
123+
raise ValueError(
124+
"{} must be a string parsable by dateutil or a datetime/date object".format(
125+
param_name
126+
)
127+
)

0 commit comments

Comments
 (0)