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

Chores/CS and linter fixes backend #457

Merged
merged 10 commits into from
Jun 11, 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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,17 @@ Contributions are very welcome. Please lint/format code before creating PRs.

### Backend

Backend is formatted using Ruff and Black.
Backend is formatted using Ruff.

```bash
pip install ruff
pip install black
```

Commands (from git root)

```bash
ruff backend
black backend
ruff check backend
ruff check backend --fix
```

### Frontend
Expand Down
35 changes: 19 additions & 16 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ repository = "https://github.com/thunderbird/appointment.git"
[project.optional-dependencies]
cli = [
"ruff",
"black"
]
db = [
"mysqlclient==2.1.1",
Expand All @@ -35,13 +34,7 @@ dependencies = { file = ["requirements.txt"] }

# Ruff
[tool.ruff]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F"]
ignore = []

# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
unfixable = []
line-length = 120

# Exclude a variety of commonly ignored directories.
exclude = [
Expand All @@ -67,22 +60,32 @@ exclude = [
"venv",
]

# Same as Black.
line-length = 120
# Always generate Python 3.11-compatible code.
target-version = "py311"

[tool.ruff.format]
# Prefer single quotes over double quotes.
quote-style = "single"

[tool.ruff.lint]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
select = ["E", "F"]
ignore = []

# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

# Assume Python 3.10.
target-version = "py310"
[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"

[tool.ruff.mccabe]
[tool.ruff.lint.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10

[tool.black]
line-length = 120

[tool.pytest.ini_options]
pythonpath = "test"

Expand Down
6 changes: 2 additions & 4 deletions backend/src/appointment/commands/create_invite_codes.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import os

from ..database import repo
from ..dependencies.database import get_engine_and_session


def run(n: int):
print(f"Generating {n} new invite codes...")
print(f'Generating {n} new invite codes...')

_, session = get_engine_and_session()
db = session()
Expand All @@ -14,4 +12,4 @@ def run(n: int):

db.close()

print(f"Successfull added {len(codes)} shiny new invite codes to the database.")
print(f'Successfull added {len(codes)} shiny new invite codes to the database.')
12 changes: 7 additions & 5 deletions backend/src/appointment/commands/download_legal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@


def run():
"""Helper function to update privacy and terms. Please check to ensure you're not getting a 404 before committing lol."""
print("Downloading the latest legal documents...")
"""Helper function to update privacy and terms.
Please check to ensure you're not getting a 404 before committing lol.
"""
print('Downloading the latest legal documents...')

extensions = ['markdown.extensions.attr_list']
devmount marked this conversation as resolved.
Show resolved Hide resolved
# Only english for now. There's no german TB privacy policy?
Expand All @@ -20,19 +22,19 @@ def run():
os.makedirs(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}', exist_ok=True)

if privacy_policy:
print("Privacy policy url found.")
print('Privacy policy url found.')
contents = requests.get(privacy_policy).text
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}/privacy.html', 'w') as fh:
fh.write(html)

if terms_of_use:
print("Terms of use url found.")
print('Terms of use url found.')
contents = requests.get(terms_of_use).text
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}/terms.html', 'w') as fh:
fh.write(html)

print("Done! Copy them over to the frontend/src/assets/legal!")
print('Done! Copy them over to the frontend/src/assets/legal!')
15 changes: 7 additions & 8 deletions backend/src/appointment/commands/update_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@


def run():
print("Checking if we have a fresh database...")
print('Checking if we have a fresh database...')

# then, load the Alembic configuration and generate the
# version table, "stamping" it with the most recent rev:
from alembic import command
from alembic.config import Config

# TODO: Does this work on stage?
alembic_cfg = Config("./alembic.ini")
alembic_cfg = Config('./alembic.ini')

# If we have our database url env variable set, use that instead!
if os.getenv("DATABASE_URL"):
alembic_cfg.set_main_option("sqlalchemy.url", os.getenv("DATABASE_URL"))
if os.getenv('DATABASE_URL'):
alembic_cfg.set_main_option('sqlalchemy.url', os.getenv('DATABASE_URL'))

engine, _ = get_engine_and_session()

Expand All @@ -31,10 +31,9 @@ def run():
# If we have no revisions, then fully create the database from the model metadata,
# and set our revision number to the latest revision. Otherwise run any new migrations
if len(revisions) == 0:
print("Initializing database, and setting it to the latest revision")
print('Initializing database, and setting it to the latest revision')
models.Base.metadata.create_all(bind=engine)
command.stamp(alembic_cfg, "head")
command.stamp(alembic_cfg, 'head')
else:
print("Database already initialized, running migrations")
print('Database already initialized, running migrations')
command.upgrade(alembic_cfg, 'head')

64 changes: 40 additions & 24 deletions backend/src/appointment/controller/apis/fxa_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def from_url(url):
# Check our supported scopes
scopes = response.get('scopes_supported')
if 'profile' not in scopes:
logging.warning("Profile scope not found in supported scopes for fxa!")
logging.warning('Profile scope not found in supported scopes for fxa!')

config = FxaConfig()
config.issuer = response.get('issuer')
Expand All @@ -44,7 +44,7 @@ class FxaClient:
ENTRYPOINT = 'tbappointment'

SCOPES = [
"profile",
'profile',
]

config = FxaConfig()
Expand All @@ -61,17 +61,25 @@ def __init__(self, client_id, client_secret, callback_url):

def setup(self, subscriber_id=None, token=None):
"""Retrieve the openid connect urls, and setup our client connection"""
if type(token) is str:
if isinstance(token, str):
token = json.loads(token)

self.config = FxaConfig.from_url(os.getenv('FXA_OPEN_ID_CONFIG'))

self.subscriber_id = subscriber_id
self.client = OAuth2Session(self.client_id, redirect_uri=self.callback_url, scope=self.SCOPES,
auto_refresh_url=self.config.token_url,
auto_refresh_kwargs={"client_id": self.client_id, "client_secret": self.client_secret, 'include_client_id': True},
token=token,
token_updater=self.token_saver)
self.client = OAuth2Session(
self.client_id,
redirect_uri=self.callback_url,
scope=self.SCOPES,
auto_refresh_url=self.config.token_url,
auto_refresh_kwargs={
'client_id': self.client_id,
'client_secret': self.client_secret,
'include_client_id': True,
},
token=token,
token_updater=self.token_saver,
)

def is_in_allow_list(self, db, email: str):
"""Check this email against our allow list"""
Expand All @@ -93,22 +101,27 @@ def get_redirect_url(self, db, state, email):
raise NotInAllowListException()

utm_campaign = f"{self.ENTRYPOINT}_{os.getenv('APP_ENV')}"
utm_source = "login"
utm_source = 'login'

try:
response = self.client.get(url=self.config.metrics_flow_url, params={
'entrypoint': self.ENTRYPOINT,
'form_type': 'email',
'utm_campaign': utm_campaign,
'utm_source': utm_source
})
response = self.client.get(
url=self.config.metrics_flow_url,
params={
'entrypoint': self.ENTRYPOINT,
'form_type': 'email',
'utm_campaign': utm_campaign,
'utm_source': utm_source,
},
)

response.raise_for_status()

flow_values = response.json()
except requests.HTTPError as e:
# Not great, but we can still continue along..
logging.error(f"Could not initialize metrics flow, error occurred: {e.response.status_code} - {e.response.text}")
logging.error(
f'Could not initialize metrics flow, error occurred: {e.response.status_code} - {e.response.text}'
)
flow_values = {}

url, state = self.client.authorization_url(
Expand All @@ -122,13 +135,15 @@ def get_redirect_url(self, db, state, email):
flow_begin_time=flow_values.get('flowBeginTime'),
flow_id=flow_values.get('flowId'),
utm_source=utm_source,
utm_campaign=utm_campaign
utm_campaign=utm_campaign,
)

return url, state

def get_credentials(self, code: str):
return self.client.fetch_token(self.config.token_url, code, client_secret=self.client_secret, include_client_id=True)
return self.client.fetch_token(
self.config.token_url, code, client_secret=self.client_secret, include_client_id=True
)

def token_saver(self, token):
"""requests-oauth automagically calls this function when it has a new refresh token for us.
Expand All @@ -141,7 +156,9 @@ def token_saver(self, token):
if self.subscriber_id is None:
return

repo.external_connection.update_token(next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.fxa)
repo.external_connection.update_token(
next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.fxa
)

def get_profile(self):
"""Retrieve the user's profile information"""
Expand All @@ -156,11 +173,10 @@ def logout(self):
raise MissingRefreshTokenException()

# This route doesn't want auth! (Because we're destroying it)
resp = requests.post(self.config.destroy_url, json={
'refresh_token': refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret
})
resp = requests.post(
self.config.destroy_url,
json={'refresh_token': refresh_token, 'client_id': self.client_id, 'client_secret': self.client_secret},
)

resp.raise_for_status()
return resp
Expand Down
Loading