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

Feat/merge fixes #13

Merged
merged 25 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f9b4587
fix(models): remove `bot_name` field
RobertRosca Sep 24, 2024
da245e9
fix(repositories): raise custom exciption when entry exists, handle i…
RobertRosca Sep 24, 2024
04ab0ba
test: set scope to session for most fixtures
RobertRosca Sep 24, 2024
1eb2675
test: expose client/zuliprc repo as fixtures
RobertRosca Sep 24, 2024
d0464b1
test: yield fastapi test client to run lifespan
RobertRosca Sep 24, 2024
48488a5
test: update mocked mymdc client `get_zulip_bot_credentials` response
RobertRosca Sep 24, 2024
cef332e
test: create session scoped event loop
RobertRosca Sep 24, 2024
d75efe7
chore: fix typo
RobertRosca Sep 24, 2024
ad0aced
test(fastapi): cleanup/disable unused tests
RobertRosca Sep 24, 2024
ecf7120
test(fastapi): update endpoint urls
RobertRosca Sep 24, 2024
c58986b
test(fastapi): update variables
RobertRosca Sep 24, 2024
f16b783
style(test/fastapi): remove unused imports
RobertRosca Sep 24, 2024
bc9317e
style(test/mymdc): add noqa flags for hardcoded secrets
RobertRosca Sep 24, 2024
62641c2
test(repositories): remove unused tests
RobertRosca Sep 24, 2024
85b6bfa
test(repositories): update for new abstract repo classes
RobertRosca Sep 24, 2024
acd087a
test(services): simplify, fix after changes
RobertRosca Sep 24, 2024
dfbdcc2
fix(api/send_message): re-add doublde newline after image upload
RobertRosca Sep 24, 2024
475623c
fix(coverage): disable coverage where appropriate
RobertRosca Sep 24, 2024
a84abfc
style: minory style changes from ruff
RobertRosca Sep 25, 2024
749b109
build(deps): tidy deps, loosen dev lint dependency constraints
RobertRosca Sep 25, 2024
a7f43fe
style: bump ruff target to py3.13, apply changes
RobertRosca Sep 25, 2024
ad5eccd
build(docker): bump to python 3.12
RobertRosca Oct 1, 2024
f0c796a
build(deps): update poetry lock
RobertRosca Oct 1, 2024
c82118e
fix(tests): make settings dotenv file path configurable
RobertRosca Oct 2, 2024
f83063e
ci(lint): run lint steps without poe, remove black
RobertRosca Oct 2, 2024
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
10 changes: 3 additions & 7 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@ jobs:

- name: Lint - ruff
if: always()
run: poetry run poe ruff

- name: Lint - black
if: always()
run: poetry run poe black --check
run: poetry run ruff check ./src ./tests

- name: Lint - mypy
if: always()
run: poetry run poe mypy
run: poetry run mypy ./src ./tests

- name: Lint - pyright
if: always()
run: poetry run poe pyright
run: poetry run pyright ./src ./tests
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ADD --link https://unpkg.com/htmx.org@1.9.10/dist/htmx.js \
https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js \
./src/zulip_write_only_proxy/frontend/static/

FROM python:3.11-alpine AS prod
FROM python:3.12-alpine AS prod

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
Expand All @@ -34,7 +34,7 @@ RUN --mount=type=cache,target=/root/.cache \
python3 -m pip install --upgrade poetry pip && \
poetry config virtualenvs.create false --local

COPY ./poetry.lock ./pyproject.toml ./README.md /app
COPY ./poetry.lock ./pyproject.toml ./README.md /app/

RUN --mount=type=cache,target=/root/.cache \
poetry install --no-root
Expand Down
1,160 changes: 667 additions & 493 deletions poetry.lock

Large diffs are not rendered by default.

14 changes: 4 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ uvicorn = "^0.23.1"
zulip = "^0.8.2"
python-multipart = "^0.0.6"
orjson = "^3.9.2"
pydantic = {extras = ["email"], version = "^2.6.1"}
pydantic = { extras = ["email"], version = "^2.6.1" }
pydantic-settings = "^2.4.0"
authlib = "^1.3.0"
itsdangerous = "^2.1.2"
Expand All @@ -39,15 +39,9 @@ debugpy = "^1.8.1"
vcrpy = "^6.0.1"

[tool.poetry.group.lint.dependencies]
ruff = "^0.1.0"
mypy = "^1.4.1"
pyright = "^1.1.320"
black = "^23.7.0"

[tool.black]
line-length = 88
target-version = ["py311", "py312"]

ruff = "^0"
mypy = "^1"
pyright = "^1"

[tool.poe.tasks]
up = { shell = "python3 -m zulip_write_only_proxy.main", env = { UVICORN_LOOP = { default = "asyncio" } } }
Expand Down
2 changes: 2 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ output-format = "grouped"

preview = true

target-version = "py313"

src = ["src", "test"]

[lint]
Expand Down
2 changes: 1 addition & 1 deletion src/zulip_write_only_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
try:
from ._version import __version__, __version_tuple__
except ImportError:
except ImportError: # pragma: no cover
__version__ = "unknown"
__version_tuple__ = (0, 0, 0) # type: ignore[assignment]

Expand Down
4 changes: 2 additions & 2 deletions src/zulip_write_only_proxy/_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
from starlette.middleware.base import BaseHTTPMiddleware
from structlog.stdlib import ProcessorFormatter

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from starlette.requests import Request
from starlette.responses import Response


def logger_name_callsite(logger, method_name, event_dict):
if not event_dict.get("logger_name"):
logger_name = f"{event_dict.pop('module')}.{event_dict.pop('func_name')}"
logger_name = f"{event_dict.pop("module")}.{event_dict.pop("func_name")}"
if not event_dict.pop("disable_name", False):
event_dict["logger_name"] = logger_name.strip(".") # pyright: ignore[reportInvalidTypeForm]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ <h2 class="card-title">Bot Created</h2>
<span>Stream: </span>
<span class="opacity-60" id="stream_name">{{ client.stream }}</span>
</div>
<div>
<span>Bot Name: </span>
<span class="opacity-60" id="bot_name">{{ client.bot_name }}</span>
</div>
<div>
<span>Bot Site: </span>
<span class="opacity-60" id="bot_name">{{ bot_site }}</span>
<span class="opacity-60" id="bot_site">{{ bot_site }}</span>
</div>
<div>
<span>Key: </span>
Expand Down
10 changes: 8 additions & 2 deletions src/zulip_write_only_proxy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ def create_app():

from . import routers, services
from ._logging import RequestLoggingMiddleware
from .settings import configure as configure_settings
from .settings import settings

configure_settings()

@asynccontextmanager
async def lifespan(app: fastapi.FastAPI):
from . import _logging, mymdc
Expand Down Expand Up @@ -47,7 +50,7 @@ async def lifespan(app: fastapi.FastAPI):
return app


def get_trusted_hosts(logger):
def get_trusted_hosts(logger): # pragma: no cover
import socket
import struct
from pathlib import Path
Expand Down Expand Up @@ -83,12 +86,15 @@ def get_trusted_hosts(logger):
return list(trusted)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
import uvicorn

from . import _logging, get_logger
from .settings import configure as configure_settings
from .settings import settings

configure_settings()

_logging.configure(settings.debug, add_call_site_parameters=False)

logger = get_logger()
Expand Down
2 changes: 1 addition & 1 deletion src/zulip_write_only_proxy/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ class Base(ABC, BaseModel):
created_at: datetime.datetime

@property
def _key(self):
def _key(self): # pragma: no cover
raise NotImplementedError
6 changes: 3 additions & 3 deletions src/zulip_write_only_proxy/models/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import operator
import secrets
from typing import IO, TYPE_CHECKING, Any

Expand All @@ -16,7 +17,7 @@
from .base import Base
from .zulip import PropagateMode

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
import zulip


Expand Down Expand Up @@ -51,7 +52,6 @@ class ScopedClient(Base):
proposal_no: int
proposal_id: int
stream: str | None # type: ignore [reportIncompatibleVariableOverride]
bot_name: str | None
bot_id: int | None
bot_site: HttpUrl | None
token: SecretStr
Expand Down Expand Up @@ -137,7 +137,7 @@ def get_messages(self):
# messages to fetch. TODO: handle multi-page results for more than 100 messages
messages = self._client.get_messages(request)
messages["messages"] = sorted(
messages["messages"], key=lambda m: m["id"], reverse=True
messages["messages"], key=operator.itemgetter("id"), reverse=True
)
return messages

Expand Down
2 changes: 1 addition & 1 deletion src/zulip_write_only_proxy/models/zulip.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .base import Base


class PropagateMode(str, enum.Enum):
class PropagateMode(enum.StrEnum):
change_one = "change_one"
change_all = "change_all"
change_later = "change_later"
Expand Down
9 changes: 5 additions & 4 deletions src/zulip_write_only_proxy/mymdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
MyMdC client package is created this can be removed and replaced with calls to that."""

import datetime as dt
from typing import TYPE_CHECKING, Any, AsyncGenerator
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING, Any

import httpx

from . import logger
from .exceptions import ZwopException
from .settings import MyMdCCredentials, Settings

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from fastapi import FastAPI


Expand All @@ -33,7 +34,7 @@ async def acquire_token(self):

Token data stored under `_access_token` and `_expires_at`.
"""
expired = self._expires_at <= dt.datetime.now(tz=dt.timezone.utc)
expired = self._expires_at <= dt.datetime.now(tz=dt.UTC)
if self._access_token and not expired:
logger.debug("Reusing existing MyMdC token", expires_at=self._expires_at)
return self._access_token
Expand Down Expand Up @@ -69,7 +70,7 @@ async def acquire_token(self):

expires_in = dt.timedelta(seconds=data["expires_in"])
self._access_token = data["access_token"]
self._expires_at = dt.datetime.now(tz=dt.timezone.utc) + expires_in
self._expires_at = dt.datetime.now(tz=dt.UTC) + expires_in

logger.info("Acquired new MyMdC token", expires_at=self._expires_at)
return self._access_token
Expand Down
11 changes: 7 additions & 4 deletions src/zulip_write_only_proxy/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
T = TypeVar("T", bound=Base)


class EntryExistsException(exceptions.ZwopException):
def __init__(self, key: str):
super().__init__(status_code=409, detail=f"Client already exists for {key}")


@dataclass
class BaseRepository(Generic[T]):
file: Path
Expand Down Expand Up @@ -93,10 +98,8 @@ async def delete(self, key: str, by: str | None = None) -> str:
async def insert(self, item: T):
if item._key in self.data:
logger.warning("Client already exists", key=item._key)
raise exceptions.ZwopException(
status_code=409,
detail=f"Client already exists for {item._key}",
)
raise EntryExistsException(key=item._key)

self._data.append(item)
self.data[item._key] = self._data[-1]

Expand Down
8 changes: 4 additions & 4 deletions src/zulip_write_only_proxy/routers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .. import __version__, __version_tuple__, logger, models, services

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from tempfile import SpooledTemporaryFile

_docs_url = "https://zulip.com/api/send-message#response"
Expand Down Expand Up @@ -70,12 +70,12 @@ def send_message(
if image:
# Some screwing around to get the spooled tmp file to act more like a real file
# since Zulip needs it to have a filename
f: "SpooledTemporaryFile" = image.file # type: ignore[assignment]
f: SpooledTemporaryFile = image.file # type: ignore[assignment]
f._file.name = image.filename # type: ignore[misc, assignment]

result = client.upload_file(f)

content += f"\n[]({result['uri']})"
content += f"\n\n[]({result["uri"]})"

return client.send_message(topic, content)

Expand Down Expand Up @@ -118,7 +118,7 @@ def upload_file(
client: Annotated[models.ScopedClient, fastapi.Depends(get_client_zulip)],
file: Annotated[fastapi.UploadFile, fastapi.File(...)],
):
f: "SpooledTemporaryFile" = file.file # type: ignore[assignment]
f: SpooledTemporaryFile = file.file # type: ignore[assignment]
f._file.name = file.filename # type: ignore[misc, assignment]

return client.upload_file(f)
Expand Down
2 changes: 1 addition & 1 deletion src/zulip_write_only_proxy/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .. import logger
from .frontend import AuthException

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from fastapi import FastAPI

from ..settings import Settings
Expand Down
8 changes: 4 additions & 4 deletions src/zulip_write_only_proxy/routers/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from .. import exceptions, logger, models, services

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no-cover
from ..settings import Settings

TEMPLATES: Jinja2Templates = None # type: ignore[assignment]
Expand Down Expand Up @@ -43,7 +43,7 @@ class AuthException(exceptions.ZwopException):
pass


async def check_auth(request: Request):
async def check_auth(request: Request): # noqa: RUF029
user = request.session.get("user")
if not user:
raise AuthException(
Expand All @@ -58,13 +58,13 @@ async def check_auth(request: Request):
if "da" not in user.get("groups", []):
raise AuthException(
status_code=403,
detail=f"Forbidden - `{user.get('preferred_username')}` not allowed access",
detail=f"Forbidden - `{user.get("preferred_username")}` not allowed access",
)

logger.debug("Authenticated", user=user)


async def auth_redirect(request: Request, exc: AuthException):
async def auth_redirect(request: Request, exc: AuthException): # noqa: RUF029
logger.info("Redirecting to login", status_code=exc.status_code, detail=exc.detail)
return TEMPLATES.TemplateResponse(
"login.html",
Expand Down
4 changes: 2 additions & 2 deletions src/zulip_write_only_proxy/routers/mymdc.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import Annotated, TypeAlias
from typing import Annotated

import orjson
from fastapi import APIRouter, Depends, HTTPException, Request, Response

from .. import logger, models, mymdc
from .api import get_client

ScopedClient: TypeAlias = Annotated[models.ScopedClient, Depends(get_client)]
type ScopedClient = Annotated[models.ScopedClient, Depends(get_client)]


async def proxy_request(mymdc_path: str, params) -> Response:
Expand Down
12 changes: 7 additions & 5 deletions src/zulip_write_only_proxy/services.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import contextlib
import datetime
import hashlib
from pathlib import Path
Expand All @@ -14,7 +15,7 @@
from .models.client import NoBotForClientError
from .settings import Settings, settings

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no-cover
from os import PathLike

CLIENT_REPO: repositories.BaseRepository[models.ScopedClient] = None # type: ignore[assignment,type-var]
Expand Down Expand Up @@ -90,11 +91,12 @@ async def get_or_create_bot(
key=SecretStr(bot_key),
email=bot_email,
site=Url(bot_site),
created_at=created_at or datetime.datetime.now(tz=datetime.timezone.utc),
created_at=created_at or datetime.datetime.now(tz=datetime.UTC),
proposal_no=proposal_no,
)

await ZULIPRC_REPO.insert(bot)
with contextlib.suppress(repositories.EntryExistsException):
await ZULIPRC_REPO.insert(bot)

return bot

Expand Down Expand Up @@ -179,8 +181,8 @@ async def get_client(key: str | None) -> models.ScopedClient:
return client


async def get_bot(bot_name: str) -> models.BotConfig | None:
return await ZULIPRC_REPO.get(bot_name)
async def get_bot(bot_key: str) -> models.BotConfig | None:
return await ZULIPRC_REPO.get(bot_key)


async def list_clients() -> list[models.ScopedClient]:
Expand Down
Loading
Loading