Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swap to Ruff #106

Merged
merged 1 commit into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 3 additions & 35 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ jobs:
strategy:
matrix:
python:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
Expand Down
31 changes: 29 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]"}
]
Expand All @@ -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"]
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",
]
9 changes: 0 additions & 9 deletions setup.cfg

This file was deleted.

11 changes: 3 additions & 8 deletions src/myob/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand All @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion src/myob/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 7 additions & 14 deletions src/myob/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 1 addition & 3 deletions src/myob/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
2 changes: 1 addition & 1 deletion src/myob/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class MyobException(Exception):
class MyobException(Exception): # noqa: N818
def __init__(self, response, msg=None):
self.response = response
try:
Expand Down
4 changes: 2 additions & 2 deletions src/myob/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))

Expand Down
30 changes: 8 additions & 22 deletions tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="},
)
Expand All @@ -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}}
Expand All @@ -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):
Expand Down Expand Up @@ -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", "/")
Expand Down Expand Up @@ -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,
{},
Expand Down Expand Up @@ -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/"
)
Expand Down Expand Up @@ -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/"
)
Expand Down Expand Up @@ -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
)
Expand Down
23 changes: 10 additions & 13 deletions tests/test_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,42 @@ 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(
{
"raw_filter": "(Type eq 'Customer' or Type eq 'Supplier') or DisplayID gt '5-0000'",
"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(
Expand Down Expand Up @@ -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")

Expand Down
Loading