diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8225464..d15ef16 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,40 +6,8 @@ on: pull_request: ~ jobs: - flake8: + ruff: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install deps - run: pip install flake8 - - name: Flake8 - run: flake8 - - isort: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install deps - run: pip install isort - - name: Isort - run: isort --diff --check-only . - - # superlinter: - # runs-on: ubuntu-latest - # steps: - # - name: Checkout code - # uses: actions/checkout@v2 - # - name: Run Superlinter - # uses: docker://github/super-linter:latest - # env: - # VALIDATE_PYTHON: true + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af8d09d..1ca1d59 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,6 @@ jobs: strategy: matrix: python: - - "3.8" - - "3.9" - "3.10" - "3.11" - "3.12" diff --git a/pyproject.toml b/pyproject.toml index a37a749..a3e7bb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "requests>=2.32.0", "requests-oauthlib>=2.0.0", ] -requires-python = ">= 3.8" +requires-python = ">= 3.10" authors = [ {name = "Jarek GÅ‚owacki", email = "jarekwg@gmail.com"} ] @@ -35,4 +35,31 @@ source = "https://github.com/uptick/pymyob" releasenotes = "https://github.com/uptick/pymyob/releases" [tool.hatch.build.targets.wheel] -packages = ["src/myob"] \ No newline at end of file +packages = ["src/myob"] + +[tool.ruff] +target-version = "py312" +line-length = 100 + +[tool.ruff.lint] +select = [ + "F", # Pyflakes + "E", # pycodestyle + "W", # pycodestyle + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + # "ANN", # flake8-annotations + "B", # flake8-bugbear + "S", # flake8-bandit + "T10", # debugger + "TID", # flake8-tidy-imports +] +ignore = [ + "E501" +] + +[tool.ruff.lint.isort] +extra-standard-library = [ + "requests", +] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 31751ae..0000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -ignore = E501 -max-line-length = 100 - -[isort] -extra_standard_library = requests -multi_line_output = 3 -include_trailing_comma = True -line_length = 100 diff --git a/src/myob/api.py b/src/myob/api.py index 42b4bc8..d8cc9a6 100755 --- a/src/myob/api.py +++ b/src/myob/api.py @@ -8,9 +8,7 @@ class Myob: def __init__(self, credentials): if not isinstance(credentials, PartnerCredentials): - raise TypeError( - f"Expected a Credentials instance, got {type(credentials).__name__}." - ) + raise TypeError(f"Expected a Credentials instance, got {type(credentials).__name__}.") self.credentials = credentials self.companyfiles = CompanyFiles(credentials) self._manager = Manager( @@ -49,8 +47,7 @@ def __init__(self, credentials): def all(self): raw_companyfiles = self._manager.all() return [ - CompanyFile(raw_companyfile, self.credentials) - for raw_companyfile in raw_companyfiles + CompanyFile(raw_companyfile, self.credentials) for raw_companyfile in raw_companyfiles ] def get(self, id, call=True): @@ -60,9 +57,7 @@ def get(self, id, call=True): # on the GET endpoint. The only way we currently allow passing company_id is by setting it on the manager, # and we can't do that on init, as this is a manager for company files plural.. # Reluctant to change manager code, as it would add confusion if the inner method let you override the company_id. - manager = Manager( - "", self.credentials, raw_endpoints=[(GET, "", "")], company_id=id - ) + manager = Manager("", self.credentials, raw_endpoints=[(GET, "", "")], company_id=id) raw_companyfile = manager.get()["CompanyFile"] else: raw_companyfile = {"Id": id} diff --git a/src/myob/constants.py b/src/myob/constants.py index 4daf014..119ee73 100644 --- a/src/myob/constants.py +++ b/src/myob/constants.py @@ -2,7 +2,7 @@ MYOB_PARTNER_BASE_URL = "https://secure.myob.com/oauth2/" AUTHORIZE_URL = "account/authorize/" -ACCESS_TOKEN_URL = "v1/authorize/" +ACCESS_TOKEN_URL = "v1/authorize/" # noqa: S105 DEFAULT_PAGE_SIZE = 400 diff --git a/src/myob/credentials.py b/src/myob/credentials.py index d020b0d..8f96534 100755 --- a/src/myob/credentials.py +++ b/src/myob/credentials.py @@ -15,7 +15,7 @@ def __init__( consumer_secret, callback_uri, verified=False, - companyfile_credentials={}, + companyfile_credentials={}, # noqa: B006 oauth_token=None, refresh_token=None, oauth_expires_at=None, @@ -32,24 +32,19 @@ def __init__( self.refresh_token = refresh_token if oauth_expires_at is not None: - assert isinstance( - oauth_expires_at, datetime.datetime - ), "'oauth_expires_at' must be a datetime instance." + if not isinstance(oauth_expires_at, datetime.datetime): + raise ValueError("'oauth_expires_at' must be a datetime instance.") self.oauth_expires_at = oauth_expires_at self._oauth = OAuth2Session(consumer_key, redirect_uri=callback_uri) - url, _ = self._oauth.authorization_url( - MYOB_PARTNER_BASE_URL + AUTHORIZE_URL, state=state - ) + url, _ = self._oauth.authorization_url(MYOB_PARTNER_BASE_URL + AUTHORIZE_URL, state=state) self.url = url + "&scope=CompanyFile" # TODO: Add `verify` kwarg here, which will quickly throw the provided credentials at a # protected endpoint to ensure they are valid. If not, raise appropriate error. def authenticate_companyfile(self, company_id, username, password): """Store hashed username-password for logging into company file.""" - userpass = base64.b64encode(bytes(f"{username}:{password}", "utf-8")).decode( - "utf-8" - ) + userpass = base64.b64encode(bytes(f"{username}:{password}", "utf-8")).decode("utf-8") self.companyfile_credentials[company_id] = userpass @property @@ -79,12 +74,10 @@ def expired(self, now=None): # Allow a bit of time for clock differences and round trip times # to prevent false negatives. If users want the precise expiry, # they can use self.oauth_expires_at - CONSERVATIVE_SECONDS = 30 + CONSERVATIVE_SECONDS = 30 # noqa: N806 now = now or datetime.datetime.now() - return self.oauth_expires_at <= ( - now + datetime.timedelta(seconds=CONSERVATIVE_SECONDS) - ) + return self.oauth_expires_at <= (now + datetime.timedelta(seconds=CONSERVATIVE_SECONDS)) def verify(self, code): """Verify an OAuth session, retrieving an access token.""" diff --git a/src/myob/endpoints.py b/src/myob/endpoints.py index 91c3d69..d8e4c0b 100755 --- a/src/myob/endpoints.py +++ b/src/myob/endpoints.py @@ -5,9 +5,7 @@ POST = "POST" PUT = "PUT" DELETE = "DELETE" -CRUD = ( - "CRUD" # shorthand for creating the ALL|GET|POST|PUT|DELETE endpoints in one swoop -) +CRUD = "CRUD" # shorthand for creating the ALL|GET|POST|PUT|DELETE endpoints in one swoop METHOD_ORDER = [ALL, GET, POST, PUT, DELETE] diff --git a/src/myob/exceptions.py b/src/myob/exceptions.py index b7bdd0c..889021c 100644 --- a/src/myob/exceptions.py +++ b/src/myob/exceptions.py @@ -1,4 +1,4 @@ -class MyobException(Exception): +class MyobException(Exception): # noqa: N818 def __init__(self, response, msg=None): self.response = response try: diff --git a/src/myob/managers.py b/src/myob/managers.py index f51c01a..fc7338d 100755 --- a/src/myob/managers.py +++ b/src/myob/managers.py @@ -18,7 +18,7 @@ class Manager: - def __init__(self, name, credentials, company_id=None, endpoints=[], raw_endpoints=[]): + def __init__(self, name, credentials, company_id=None, endpoints=[], raw_endpoints=[]): # noqa: B006 self.credentials = credentials self.name = "_".join(p for p in name.rstrip("/").split("/") if "[" not in p) self.base_url = MYOB_BASE_URL @@ -192,7 +192,7 @@ def build_value(value): if k.endswith(f"__{op}"): k = k[:-4] operator = op - if not isinstance(v, (list, tuple)): + if not isinstance(v, list | tuple): v = [v] filters.append(" or ".join(f"{k} {operator} {build_value(v_)}" for v_ in v)) diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 3976d7d..d306d2e 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -26,7 +26,7 @@ class EndpointTests(TestCase): def setUp(self): cred = PartnerCredentials( consumer_key="KeyToTheKingdom", - consumer_secret="TellNoOne", + consumer_secret="TellNoOne", # noqa: S106 callback_uri="CallOnlyWhenCalledTo", companyfile_credentials={CID: "!encoded-userpass="}, ) @@ -40,9 +40,7 @@ def setUp(self): } @patch("myob.managers.requests.request") - def assertEndpointReached( - self, func, params, method, endpoint, mock_request, timeout=None - ): + def assertEndpointReached(self, func, params, method, endpoint, mock_request, timeout=None): # noqa: N802 mock_request.return_value.status_code = 200 if endpoint == f"/{CID}/": mock_request.return_value.json.return_value = {"CompanyFile": {"Id": CID}} @@ -58,9 +56,7 @@ def assertEndpointReached( ) @patch("myob.managers.requests.request") - def assertExceptionHandled( - self, status_code, response_json, exception, mock_request - ): + def assertExceptionHandled(self, status_code, response_json, exception, mock_request): # noqa: N802 mock_request.return_value.status_code = status_code mock_request.return_value.json.return_value = response_json with self.assertRaises(exception): @@ -121,9 +117,7 @@ def test_companyfiles(self): " get(id) - List endpoints available for a company file." ), ) - self.assertEndpointReached( - self.myob.companyfiles.get, {"id": CID}, "GET", f"/{CID}/" - ) + self.assertEndpointReached(self.myob.companyfiles.get, {"id": CID}, "GET", f"/{CID}/") # Don't expect companyfile credentials here as the next endpoint is not companyfile specific. del self.expected_request_headers["x-myobapi-cftoken"] self.assertEndpointReached(self.myob.companyfiles.all, {}, "GET", "/") @@ -175,9 +169,7 @@ def test_banking(self): " transfermoneytxn() - Return all transfer money transactions for an AccountRight company file." ), ) - self.assertEndpointReached( - self.companyfile.banking.all, {}, "GET", f"/{CID}/Banking/" - ) + self.assertEndpointReached(self.companyfile.banking.all, {}, "GET", f"/{CID}/Banking/") self.assertEndpointReached( self.companyfile.banking.spendmoneytxn, {}, @@ -292,9 +284,7 @@ def test_contacts(self): " supplier() - Return all supplier contacts for an AccountRight company file." ), ) - self.assertEndpointReached( - self.companyfile.contacts.all, {}, "GET", f"/{CID}/Contact/" - ) + self.assertEndpointReached(self.companyfile.contacts.all, {}, "GET", f"/{CID}/Contact/") self.assertEndpointReached( self.companyfile.contacts.customer, {}, "GET", f"/{CID}/Contact/Customer/" ) @@ -579,9 +569,7 @@ def test_quotes(self): " service() - Return all service type sale quotes for an AccountRight company file." ), ) - self.assertEndpointReached( - self.companyfile.quotes.all, {}, "GET", f"/{CID}/Sale/Quote/" - ) + self.assertEndpointReached(self.companyfile.quotes.all, {}, "GET", f"/{CID}/Sale/Quote/") self.assertEndpointReached( self.companyfile.quotes.item, {}, "GET", f"/{CID}/Sale/Quote/Item/" ) @@ -1298,9 +1286,7 @@ def test_timeout(self): def test_exceptions(self): self.assertExceptionHandled(400, {}, MyobBadRequest) self.assertExceptionHandled(401, {}, MyobUnauthorized) - self.assertExceptionHandled( - 403, {"Errors": [{"Name": "Something"}]}, MyobForbidden - ) + self.assertExceptionHandled(403, {"Errors": [{"Name": "Something"}]}, MyobForbidden) self.assertExceptionHandled( 403, {"Errors": [{"Name": "RateLimitError"}]}, MyobRateLimitExceeded ) diff --git a/tests/test_managers.py b/tests/test_managers.py index 6ed8467..aaceeec 100644 --- a/tests/test_managers.py +++ b/tests/test_managers.py @@ -10,37 +10,31 @@ class QueryParamTests(TestCase): def setUp(self): cred = PartnerCredentials( consumer_key="KeyToTheKingdom", - consumer_secret="TellNoOne", + consumer_secret="TellNoOne", # noqa: S106 callback_uri="CallOnlyWhenCalledTo", ) self.manager = Manager("", credentials=cred) - def assertParamsEqual(self, raw_kwargs, expected_params, method="GET"): + def assertParamsEqual(self, raw_kwargs, expected_params, method="GET"): # noqa: N802 self.assertEqual( self.manager.build_request_kwargs(method, {}, **raw_kwargs)["params"], expected_params, ) def test_filter(self): - self.assertParamsEqual( - {"Type": "Customer"}, {"$filter": "(Type eq 'Customer')"} - ) + self.assertParamsEqual({"Type": "Customer"}, {"$filter": "(Type eq 'Customer')"}) self.assertParamsEqual( {"Type": ["Customer", "Supplier"]}, {"$filter": "(Type eq 'Customer' or Type eq 'Supplier')"}, ) - self.assertParamsEqual( - {"DisplayID__gt": "5-0000"}, {"$filter": "(DisplayID gt '5-0000')"} - ) + self.assertParamsEqual({"DisplayID__gt": "5-0000"}, {"$filter": "(DisplayID gt '5-0000')"}) self.assertParamsEqual( {"DateOccurred__lt": "2013-08-30T19:00:59.043"}, {"$filter": "(DateOccurred lt '2013-08-30T19:00:59.043')"}, ) self.assertParamsEqual( {"Type": ("Customer", "Supplier"), "DisplayID__gt": "5-0000"}, - { - "$filter": "(Type eq 'Customer' or Type eq 'Supplier') and (DisplayID gt '5-0000')" - }, + {"$filter": "(Type eq 'Customer' or Type eq 'Supplier') and (DisplayID gt '5-0000')"}, ) self.assertParamsEqual( { @@ -48,7 +42,10 @@ def test_filter(self): "DateOccurred__lt": "2013-08-30T19:00:59.043", }, { - "$filter": "((Type eq 'Customer' or Type eq 'Supplier') or DisplayID gt '5-0000') and (DateOccurred lt '2013-08-30T19:00:59.043')" + "$filter": ( + "((Type eq 'Customer' or Type eq 'Supplier') or DisplayID gt '5-0000') " + "and (DateOccurred lt '2013-08-30T19:00:59.043')" + ) }, ) self.assertParamsEqual( @@ -85,7 +82,7 @@ def test_templatename(self): {"templatename": "InvoiceTemplate - 7"}, ) - def test_returnBody(self): + def test_returnbody(self): self.assertParamsEqual({}, {"returnBody": "true"}, method="PUT") self.assertParamsEqual({}, {"returnBody": "true"}, method="POST")