From 9817506a28f708e7335f08ddd4b51ce157cf4e07 Mon Sep 17 00:00:00 2001 From: Pedro Impulcetto - Indeed Date: Wed, 22 May 2024 17:00:25 -0300 Subject: [PATCH 1/4] feat: including typehints; tox to manage packages; mypy to static check; remove unnecessary requirements; change circle ci to use tox; run black and isort to format --- .circleci/config.yml | 7 +- closeio_api/__init__.py | 230 +++++++++++++++++++++++++--------------- closeio_api/utils.py | 10 +- mypy.ini | 3 + requirements.txt | 3 - setup.py | 9 +- tests/test_api.py | 171 +++++++++++++++-------------- tox.ini | 41 +++++++ 8 files changed, 291 insertions(+), 183 deletions(-) create mode 100644 mypy.ini create mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml index fb252e4..67239e7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,10 +15,13 @@ defaults: &defaults - checkout - run: name: Install dependencies - command: pip install -r requirements.txt + command: pip install tox - run: name: Test - command: pytest + command: tox -e py + - run: + name: Mypy + command: tox -e mypy jobs: test-3.7: diff --git a/closeio_api/__init__.py b/closeio_api/__init__.py index f0b912f..38a84ff 100644 --- a/closeio_api/__init__.py +++ b/closeio_api/__init__.py @@ -2,23 +2,26 @@ import logging import re import time - from random import uniform +from typing import Any, Dict, Literal, Optional, Union, Unpack import requests from closeio_api.utils import local_tz_offset -DEFAULT_RATE_LIMIT_DELAY = 2 # Seconds +RequestMethod = Literal["get", "post", "put", "patch", "delete"] + +DEFAULT_RATE_LIMIT_DELAY = 2 # Seconds # To update the package version, change this variable. This variable is also # read by setup.py when installing the package. -__version__ = '2.1' +__version__ = "2.1" + class APIError(Exception): """Raised when sending a request to the API failed.""" - def __init__(self, response): + def __init__(self, response: requests.Response) -> None: # For compatibility purposes we can access the original string through # the args property. super(APIError, self).__init__(response.text) @@ -28,20 +31,26 @@ def __init__(self, response): class ValidationError(APIError): """Raised when the API returns validation errors.""" - def __init__(self, response): + def __init__(self, response: requests.Response) -> None: super(ValidationError, self).__init__(response) # Easy access to errors. - data = response.json() - self.errors = data.get('errors', []) - self.field_errors = data.get('field-errors', {}) + data: Dict[Any, Any] = response.json() + self.errors = data.get("errors", []) + self.field_errors = data.get("field-errors", {}) class API(object): """Main class interacting with the Close API.""" - def __init__(self, base_url, api_key=None, tz_offset=None, max_retries=5, - verify=True): + def __init__( + self, + base_url: str, + api_key: Optional[str] = None, + tz_offset: Optional[str] = None, + max_retries: int = 5, + verify: bool = True, + ) -> None: assert base_url self.base_url = base_url self.max_retries = max_retries @@ -50,40 +59,41 @@ def __init__(self, base_url, api_key=None, tz_offset=None, max_retries=5, self.session = requests.Session() if api_key: - self.session.auth = (api_key, '') - - self.session.headers.update({ - 'User-Agent': 'Close/{} python ({})'.format( - __version__, - requests.utils.default_user_agent() - ), - 'X-TZ-Offset': self.tz_offset - }) - - def _prepare_request(self, method_name, endpoint, api_key=None, data=None, - debug=False, **kwargs): + self.session.auth = (api_key, "") + + self.session.headers.update( + { + "User-Agent": "Close/{} python ({})".format( + __version__, requests.utils.default_user_agent() + ), + "X-TZ-Offset": self.tz_offset, + } + ) + + def _prepare_request( + self, + method_name: RequestMethod, + endpoint: str, + api_key: Optional[str] = None, + data: Optional[Dict[Any, Any]] = None, + debug: bool = False, + **kwargs: Any + ) -> requests.PreparedRequest: """Construct and return a requests.Request object based on provided parameters. """ if api_key: - auth = (api_key, '') + auth = (api_key, "") else: auth = None - assert self.session.auth, 'Must specify api_key.' + assert self.session.auth, "Must specify api_key." - headers = kwargs.pop('headers', {}) + headers = kwargs.pop("headers", {}) if data: - headers.update({ - 'Content-Type': 'application/json' - }) - - kwargs.update({ - 'auth': auth, - 'headers': headers, - 'json': data - }) - request = requests.Request(method_name, self.base_url + endpoint, - **kwargs) + headers.update({"Content-Type": "application/json"}) + + kwargs.update({"auth": auth, "headers": headers, "json": data}) + request = requests.Request(method_name, self.base_url + endpoint, **kwargs) prepped_request = self.session.prepare_request(request) if debug: @@ -91,18 +101,29 @@ def _prepare_request(self, method_name, endpoint, api_key=None, data=None, return prepped_request - def _dispatch(self, method_name, endpoint, api_key=None, data=None, - debug=False, timeout=None, **kwargs): + def _dispatch( + self, + method_name: RequestMethod, + endpoint: str, + api_key: Optional[str] = None, + data: Optional[Dict[Any, Any]] = None, + debug: bool = False, + timeout: Optional[int] = None, + **kwargs: Any + ) -> Any: """Prepare and send a request with given parameters. Return a dict containing the response data or raise an exception if any errors occurred. """ - prepped_req = self._prepare_request(method_name, endpoint, api_key, - data, debug, **kwargs) + prepped_req = self._prepare_request( + method_name, endpoint, api_key, data, debug, **kwargs + ) for retry_count in range(self.max_retries): try: - response = self.session.send(prepped_req, verify=self.verify, timeout=timeout) + response = self.session.send( + prepped_req, verify=self.verify, timeout=timeout + ) except requests.exceptions.ConnectionError: if retry_count + 1 == self.max_retries: raise @@ -111,19 +132,21 @@ def _dispatch(self, method_name, endpoint, api_key=None, data=None, # Check if request was rate limited. if response.status_code == 429: sleep_time = self._get_rate_limit_sleep_time(response) - logging.debug('Request was rate limited, sleeping %d seconds', sleep_time) + logging.debug( + "Request was rate limited, sleeping %d seconds", sleep_time + ) time.sleep(sleep_time) continue - - # Retry 503 errors or 502 or 504 erors on GET requests. + + # Retry 503 errors or 502 or 504 erors on GET requests. elif response.status_code == 503 or ( - method_name == 'get' and response.status_code in (502, 504) + method_name == "get" and response.status_code in (502, 504) ): sleep_time = self._get_randomized_sleep_time_for_error( response.status_code, retry_count ) logging.debug( - 'Request hit a {}, sleeping for {} seconds'.format( + "Request hit a {}, sleeping for {} seconds".format( response.status_code, sleep_time ) ) @@ -134,26 +157,24 @@ def _dispatch(self, method_name, endpoint, api_key=None, data=None, break if response.ok: - # 204 responses have no content. + # 204 responses have no content. if response.status_code == 204: - return '' + return "" return response.json() elif response.status_code == 400: raise ValidationError(response) else: raise APIError(response) - def _get_rate_limit_sleep_time(self, response): + def _get_rate_limit_sleep_time(self, response: requests.Response) -> float: """Get rate limit window expiration time from response if the response - status code is 429. + status code is 429. """ with contextlib.suppress(KeyError): rate_limit = response.headers["RateLimit"] # we don't actually need all these values, but per the RFC: # "Malformed RateLimit header fields MUST be ignored." - match = re.match( - r"limit=(\d+), remaining=(\d+), reset=(\d+)", rate_limit - ) + match = re.match(r"limit=(\d+), remaining=(\d+), reset=(\d+)", rate_limit) if match: limit, remaining, reset = match.groups() return float(reset) @@ -164,24 +185,32 @@ def _get_rate_limit_sleep_time(self, response): with contextlib.suppress(KeyError): return float(response.headers["RateLimit-Reset"]) - logging.exception('Error parsing rate limiting response') + logging.exception("Error parsing rate limiting response") return DEFAULT_RATE_LIMIT_DELAY - def _get_randomized_sleep_time_for_error(self, status_code, retries): + def _get_randomized_sleep_time_for_error( + self, status_code: int, retries: int + ) -> Union[int, float]: """Get sleep time for a given status code before we can try the request again. - - Each time we retry, we want to increase the time before we try again. + + Each time we retry, we want to increase the time before we try again. """ if status_code == 503: return uniform(2, 4) * (retries + 1) - + elif status_code in (502, 504): return uniform(60, 90) * (retries + 1) - + return DEFAULT_RATE_LIMIT_DELAY - - def get(self, endpoint, params=None, timeout=None, **kwargs): + + def get( + self, + endpoint: str, + params: Optional[Dict[Any, Any]] = None, + timeout: Optional[int] = None, + **kwargs: Any + ) -> Any: """Send a GET request to a given endpoint, for example: >>> api.get('lead', {'query': 'status:"Potential"'}) @@ -193,10 +222,16 @@ def get(self, endpoint, params=None, timeout=None, **kwargs): ] } """ - kwargs.update({'params': params}) - return self._dispatch('get', endpoint+'/', timeout=timeout, **kwargs) - - def post(self, endpoint, data, timeout=None, **kwargs): + kwargs.update({"params": params}) + return self._dispatch("get", endpoint + "/", timeout=timeout, **kwargs) + + def post( + self, + endpoint: str, + data: Dict[Any, Any], + timeout: Optional[int] = None, + **kwargs: Any + ) -> Any: """Send a POST request to a given endpoint, for example: >>> api.post('lead', {'name': 'Brand New Lead'}) @@ -205,10 +240,16 @@ def post(self, endpoint, data, timeout=None, **kwargs): # ... rest of the response omitted for brevity } """ - kwargs.update({'data': data}) - return self._dispatch('post', endpoint+'/', timeout=timeout, **kwargs) - - def put(self, endpoint, data, timeout=None, **kwargs): + kwargs.update({"data": data}) + return self._dispatch("post", endpoint + "/", timeout=timeout, **kwargs) + + def put( + self, + endpoint: str, + data: Dict[Any, Any], + timeout: Optional[int] = None, + **kwargs: Any + ) -> Any: """Send a PUT request to a given endpoint, for example: >>> api.put('lead/SOME_LEAD_ID', {'name': 'New Name'}) @@ -217,38 +258,51 @@ def put(self, endpoint, data, timeout=None, **kwargs): # ... rest of the response omitted for brevity } """ - kwargs.update({'data': data}) - return self._dispatch('put', endpoint+'/', timeout=timeout, **kwargs) + kwargs.update({"data": data}) + return self._dispatch("put", endpoint + "/", timeout=timeout, **kwargs) - def delete(self, endpoint, timeout=None, **kwargs): + def delete( + self, endpoint: str, timeout: Optional[int] = None, **kwargs: Any + ) -> Any: """Send a DELETE request to a given endpoint, for example: >>> api.delete('lead/SOME_LEAD_ID') {} """ - return self._dispatch('delete', endpoint+'/', timeout=timeout, **kwargs) + return self._dispatch("delete", endpoint + "/", timeout=timeout, **kwargs) - def _print_request(self, req): + def _print_request(self, req: requests.PreparedRequest) -> None: """Print a human-readable representation of a request.""" - print('{}\n{}\n{}\n\n{}\n{}'.format( - '----------- HTTP Request -----------', - req.method + ' ' + req.url, - '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()), - req.body or '', - '----------- /HTTP Request -----------' - )) + print( + "{}\n{}\n{}\n\n{}\n{}".format( + "----------- HTTP Request -----------", + req.method + " " + req.url, # type: ignore + "\n".join("{}: {}".format(k, v) for k, v in req.headers.items()), + req.body or "", + "----------- /HTTP Request -----------", + ) + ) class Client(API): - def __init__(self, api_key=None, tz_offset=None, max_retries=5, - development=False): + def __init__( + self, + api_key: Optional[str] = None, + tz_offset: Optional[str] = None, + max_retries: int = 5, + development: bool = False, + ) -> None: if development: - base_url = 'https://local-api.close.com:5001/api/v1/' + base_url = "https://local-api.close.com:5001/api/v1/" # See https://github.com/kennethreitz/requests/issues/2966 verify = False else: - base_url = 'https://api.close.com/api/v1/' + base_url = "https://api.close.com/api/v1/" verify = True - super(Client, self).__init__(base_url, api_key, tz_offset=tz_offset, - max_retries=max_retries, verify=verify) - + super(Client, self).__init__( + base_url, + api_key, + tz_offset=tz_offset, + max_retries=max_retries, + verify=verify, + ) diff --git a/closeio_api/utils.py b/closeio_api/utils.py index 42b2b60..faf6d18 100644 --- a/closeio_api/utils.py +++ b/closeio_api/utils.py @@ -1,7 +1,11 @@ import time -def local_tz_offset(): +def local_tz_offset() -> float: # http://stackoverflow.com/questions/1111056/get-tz-information-of-the-system-in-python - return (time.timezone if (time.localtime().tm_isdst == 0) - else time.altzone) / 60 / 60 * -1 + return ( + (time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) + / 60 + / 60 + * -1 + ) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..d696748 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] + +strict = True diff --git a/requirements.txt b/requirements.txt index 83c59b5..a743bbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1 @@ -pytest==7.4.0 -pytest-cov==4.1.0 requests==2.27.1 -responses==0.20.0 diff --git a/setup.py b/setup.py index 038a0f6..7a26845 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import io import re + from setuptools import setup VERSION_FILE = "closeio_api/__init__.py" @@ -8,15 +9,13 @@ setup( name="closeio", - packages=['closeio_api'], + packages=["closeio_api"], version=version, description="Close API Python Client", long_description="Close API Python Client", author="Close Team", url="https://github.com/closeio/closeio-api/", - install_requires=[ - 'requests >= 2.11.1' - ], + install_requires=["requests >= 2.11.1"], classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", @@ -25,5 +24,5 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Operating System :: OS Independent", - ] + ], ) diff --git a/tests/test_api.py b/tests/test_api.py index c08127e..70df8d0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,112 +1,121 @@ import json +from unittest import mock import pytest - -import responses import requests +import responses + from closeio_api import APIError, Client -from unittest import mock SAMPLE_LEAD_RESPONSE = { - 'name': 'Sample Lead', - 'contacts': [], + "name": "Sample Lead", + "contacts": [], # Other lead fields omitted for brevity } -SAMPLE_LEADS_RESPONSE = { - 'has_more': False, - 'data': [SAMPLE_LEAD_RESPONSE] -} +SAMPLE_LEADS_RESPONSE = {"has_more": False, "data": [SAMPLE_LEAD_RESPONSE]} @pytest.fixture def api_client(): """Return the Close API client fixture.""" - return Client('fake-api-key') + return Client("fake-api-key") + @responses.activate def test_list_leads(api_client): responses.add( responses.GET, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", body=json.dumps(SAMPLE_LEADS_RESPONSE), status=200, - content_type='application/json' + content_type="application/json", ) - resp = api_client.get('lead') - assert not resp['has_more'] - assert resp['data'][0]['name'] == 'Sample Lead' + resp = api_client.get("lead") + assert not resp["has_more"] + assert resp["data"][0]["name"] == "Sample Lead" + @responses.activate def test_fetch_lead(api_client): responses.add( responses.GET, - 'https://api.close.com/api/v1/lead/lead_abcdefghijklmnop/', + "https://api.close.com/api/v1/lead/lead_abcdefghijklmnop/", body=json.dumps(SAMPLE_LEAD_RESPONSE), status=200, - content_type='application/json' + content_type="application/json", ) - resp = api_client.get('lead/lead_abcdefghijklmnop') - assert resp['name'] == 'Sample Lead' + resp = api_client.get("lead/lead_abcdefghijklmnop") + assert resp["name"] == "Sample Lead" + @responses.activate def test_create_lead(api_client): def request_callback(request): - payload = json.loads(request.body.decode('UTF-8')) - expected_payload = {'name': 'Sample Lead'} + payload = json.loads(request.body.decode("UTF-8")) + expected_payload = {"name": "Sample Lead"} assert payload == expected_payload return (200, {}, json.dumps(payload)) responses.add_callback( responses.POST, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", callback=request_callback, - content_type='application/json', + content_type="application/json", ) - resp = api_client.post('lead', {'name': 'Sample Lead'}) - assert resp['name'] == 'Sample Lead' + resp = api_client.post("lead", {"name": "Sample Lead"}) + assert resp["name"] == "Sample Lead" + @responses.activate def test_failed_create_lead(api_client): responses.add( responses.POST, - 'https://api.close.com/api/v1/lead/', - body='Forbidden', + "https://api.close.com/api/v1/lead/", + body="Forbidden", status=403, - content_type='application/json' + content_type="application/json", ) with pytest.raises(APIError): - resp = api_client.post('lead', {'name': 'Sample Lead'}) + resp = api_client.post("lead", {"name": "Sample Lead"}) + @responses.activate def test_search_for_leads(api_client): def request_callback(request): - assert request.url == 'https://api.close.com/api/v1/lead/?query=name%3Asample' - return (200, {}, json.dumps({ - 'has_more': False, - 'data': [ + assert request.url == "https://api.close.com/api/v1/lead/?query=name%3Asample" + return ( + 200, + {}, + json.dumps( { - 'name': 'Sample Lead', - 'contacts': [], - # Other lead fields omitted for brevity + "has_more": False, + "data": [ + { + "name": "Sample Lead", + "contacts": [], + # Other lead fields omitted for brevity + } + ], } - ] - })) + ), + ) responses.add_callback( responses.GET, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", callback=request_callback, - content_type='application/json', + content_type="application/json", ) - resp = api_client.get('lead', params={'query': 'name:sample'}) - assert not resp['has_more'] - assert resp['data'][0]['name'] == 'Sample Lead' + resp = api_client.get("lead", params={"query": "name:sample"}) + assert not resp["has_more"] + assert resp["data"][0]["name"] == "Sample Lead" + @responses.activate @pytest.mark.parametrize( @@ -120,32 +129,32 @@ def request_callback(request): "RateLimit-Reset": "1", "RateLimit": "limit=100, remaining=0, reset=1", }, - ] + ], ) def test_retry_on_rate_limit(api_client, headers): - with mock.patch('time.sleep'): + with mock.patch("time.sleep"): with responses.RequestsMock() as rsps: # Rate limit the first request and suggest it can be retried in 1 sec. rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/lead_abcdefghijklmnop/', + "https://api.close.com/api/v1/lead/lead_abcdefghijklmnop/", body=json.dumps({}), status=429, - content_type='application/json', + content_type="application/json", headers=headers, ) # Respond correctly to the second request. rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/lead_abcdefghijklmnop/', + "https://api.close.com/api/v1/lead/lead_abcdefghijklmnop/", body=json.dumps(SAMPLE_LEAD_RESPONSE), status=200, - content_type='application/json' + content_type="application/json", ) - resp = api_client.get('lead/lead_abcdefghijklmnop') - assert resp['name'] == 'Sample Lead' + resp = api_client.get("lead/lead_abcdefghijklmnop") + assert resp["name"] == "Sample Lead" # Make sure two calls were made to the API (one rate limited and one # successful). @@ -154,66 +163,68 @@ def test_retry_on_rate_limit(api_client, headers): @responses.activate def test_retry_on_connection_error(api_client): - with mock.patch('time.sleep'): + with mock.patch("time.sleep"): with responses.RequestsMock() as rsps: rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/', - body=requests.ConnectionError() + "https://api.close.com/api/v1/lead/", + body=requests.ConnectionError(), ) rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", body=json.dumps(SAMPLE_LEADS_RESPONSE), status=200, - content_type='application/json' + content_type="application/json", ) - resp = api_client.get('lead') - assert resp['data'][0]['name'] == 'Sample Lead' + resp = api_client.get("lead") + assert resp["data"][0]["name"] == "Sample Lead" # Make sure two calls were made to the API (one connection error and one successful). assert len(rsps.calls) == 2 + @responses.activate def test_max_retry_connection_error(api_client): # retrying 5 times is a bit slow - skip the waiting - with mock.patch('time.sleep'): + with mock.patch("time.sleep"): with responses.RequestsMock() as rsps: rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/', - body=requests.ConnectionError() + "https://api.close.com/api/v1/lead/", + body=requests.ConnectionError(), ) with pytest.raises(requests.exceptions.ConnectionError): - api_client.get('lead') + api_client.get("lead") # Make sure max calls were made assert len(rsps.calls) == 5 + @responses.activate def test_raises_api_error_on_max_retry(api_client): # retrying 5 times is a bit slow - skip the waiting - with mock.patch('time.sleep'): + with mock.patch("time.sleep"): with responses.RequestsMock() as rsps: rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", status=503, ) rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", status=502, ) rsps.add( responses.GET, - 'https://api.close.com/api/v1/lead/', + "https://api.close.com/api/v1/lead/", status=504, ) with pytest.raises(APIError): - api_client.get('lead') + api_client.get("lead") # Make sure max calls were made assert len(rsps.calls) == 5 @@ -223,30 +234,26 @@ def test_raises_api_error_on_max_retry(api_client): def test_validation_error(api_client): responses.add( responses.POST, - 'https://api.close.com/api/v1/contact/', - body=json.dumps({ - 'errors': [], - 'field-errors': { - 'lead': 'This field is required.' - } - }), + "https://api.close.com/api/v1/contact/", + body=json.dumps( + {"errors": [], "field-errors": {"lead": "This field is required."}} + ), status=400, - content_type='application/json' + content_type="application/json", ) with pytest.raises(APIError) as excinfo: - api_client.post('contact', {'name': 'new lead'}) + api_client.post("contact", {"name": "new lead"}) err = excinfo.value assert err.errors == [] - assert err.field_errors['lead'] == 'This field is required.' + assert err.field_errors["lead"] == "This field is required." + @responses.activate def test_204_responses(api_client): responses.add( - responses.DELETE, - "https://api.close.com/api/v1/pipeline/pipe_1234/", - status=204 + responses.DELETE, "https://api.close.com/api/v1/pipeline/pipe_1234/", status=204 ) - resp = api_client.delete('pipeline/pipe_1234') - assert resp == '' + resp = api_client.delete("pipeline/pipe_1234") + assert resp == "" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..727f660 --- /dev/null +++ b/tox.ini @@ -0,0 +1,41 @@ +[tox] +skipsdist = true + +# These envs are run in order if you just run `tox` +envlist = + format # runs auto-formatter - black + lint # lints + mypy # type checking + py # tests + +[base] +deps = + -r requirements.txt + +[testenv:py] +deps = + pytest + pytest-cov + responses + {[base]deps} + +commands = + pytest + +[testenv:format] +deps = + black + isort +commands = + black . + isort . +description = Run linters. + +[testenv:mypy] +deps = + mypy + typing-extensions + types-requests +commands = + mypy closeio_api +description = Run the mypy tool to check static typing on the project. \ No newline at end of file From 8969501f202b548b1bfd32be48fa7d63a8705d4b Mon Sep 17 00:00:00 2001 From: Pedro Impulcetto - Indeed Date: Thu, 23 May 2024 09:54:55 -0300 Subject: [PATCH 2/4] docs: including contributions section in readme --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 24324b1..a76a07e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,44 @@ lead_results = api.get('lead', params={ Check out [https://github.com/closeio/closeio-api-scripts](https://github.com/closeio/closeio-api-scripts) for helpful scripts already written to accomplish some common tasks. + +### Contributions +We welcome contributions to this project! To get started, follow these steps to set up your environment: + +- Fork the repository + +- Create a virtual environment +```sh +python3 -m venv venv +source source venv/bin/activate +``` +- Install dependencies +```sh +pip install -r requirements.txt +``` + +- Install `tox` for development purposes +```sh +pip install tox +``` + +- Run `tox` to execute tests, type checks, and formatters +```sh +tox +``` + - To run only tests: +```sh +tox -e py +``` + - To run only the formatter: +```sh +tox -e format +``` + - To run only type checks with mypy: +```sh +tox -e mypy +``` + ### Other Languages There are unofficial API clients available in other languages too, thanks to some awesome contributors: From 322cfc4e10fbefa6ecac738b66c0044ca6f559c0 Mon Sep 17 00:00:00 2001 From: Pedro Impulcetto - Indeed Date: Thu, 23 May 2024 10:39:46 -0300 Subject: [PATCH 3/4] fix: tox init --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 727f660..74b7b15 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,7 @@ skipsdist = true # These envs are run in order if you just run `tox` envlist = - format # runs auto-formatter - black - lint # lints + format # runs auto-formatter - black and isort mypy # type checking py # tests From f26d8a699e7f3f04bd85278557ea0a7a7982f88b Mon Sep 17 00:00:00 2001 From: Pedro Impulcetto - Indeed Date: Thu, 23 May 2024 13:04:48 -0300 Subject: [PATCH 4/4] fix: removing unusable imports --- closeio_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/closeio_api/__init__.py b/closeio_api/__init__.py index 38a84ff..e8280cd 100644 --- a/closeio_api/__init__.py +++ b/closeio_api/__init__.py @@ -3,7 +3,7 @@ import re import time from random import uniform -from typing import Any, Dict, Literal, Optional, Union, Unpack +from typing import Any, Dict, Literal, Optional, Union import requests