Skip to content

Commit

Permalink
project scaffold
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-oleshkevich committed Mar 20, 2024
1 parent 9b2b4e3 commit 3ef98d2
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Run pre-commit
uses: pre-commit/action@v3
uses: pre-commit/action@v3.0.1

unit_tests:
runs-on: ubuntu-latest
Expand Down
9 changes: 6 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ repos:
- id: check-executables-have-shebangs

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.7
rev: v0.3.3
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.7.1'
rev: 'v1.9.0'
hooks:
- id: mypy
files: "ohmyadmin"
Expand All @@ -41,7 +41,10 @@ repos:
- starlette
- starlette_babel
- starlette_flash
- "sqlalchemy==2.0.0rc1"
- types-beautifulsoup4
- types-passlib
- types-wtforms
- "sqlalchemy==2"

- repo: https://github.com/myint/docformatter.git
rev: v1.7.5
Expand Down
4 changes: 2 additions & 2 deletions examples/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sqlalchemy as sa
from async_storages import FileStorage, FileSystemBackend
from passlib.handlers.pbkdf2 import pbkdf2_sha256
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine
from starception import install_error_handler
from starlette.applications import Starlette
from starlette.authentication import BaseUser
Expand Down Expand Up @@ -33,7 +33,7 @@ def index_view(request: Request) -> Response:


class DatabaseSessionMiddleware:
def __init__(self, app: ASGIApp, sessionmaker: async_sessionmaker) -> None:
def __init__(self, app: ASGIApp, sessionmaker: async_sessionmaker[AsyncSession]) -> None:
self.app = app
self.sessionmaker = sessionmaker

Expand Down
4 changes: 2 additions & 2 deletions examples/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ def seed_product_categories(session: AsyncSession) -> None:
session.add_all(
[
ProductCategory(
product_id=random.randint(1, OBJECTS_COUNT), # type: ignore[call-arg]
category_id=random.randint(1, 20), # type: ignore[call-arg]
product_id=random.randint(1, OBJECTS_COUNT),
category_id=random.randint(1, 20),
)
for _ in range(1, (OBJECTS_COUNT + 1 * 20))
]
Expand Down
4 changes: 1 addition & 3 deletions examples/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@

config = Config()

DATABASE_URL = config(
"DATABASE_URL", default="postgresql+asyncpg://postgres:postgres@localhost/ohmyadmin"
)
DATABASE_URL = config("DATABASE_URL", default="postgresql+asyncpg://postgres:postgres@localhost/ohmyadmin")
16 changes: 9 additions & 7 deletions ohmyadmin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import typing

import jinja2
from async_storages import FileStorage
from async_storages import FileStorage, MemoryBackend
from async_storages.contrib.starlette import FileServer
from starlette import templating
from starlette.middleware import Middleware
Expand Down Expand Up @@ -36,13 +36,13 @@ def __init__(
theme: Theme = Theme(),
file_storage: FileStorage | None = None,
auth_policy: AuthPolicy | None = None,
template_dirs: typing.Sequence[str | os.PathLike] = tuple(),
template_dirs: typing.Sequence[str | os.PathLike[str]] = tuple(),
template_packages: typing.Sequence[str] = tuple(),
) -> None:
self.pages = pages
self.theme = theme
self.app_name = app_name
self.file_storage = file_storage
self.file_storage = file_storage or FileStorage(storage=MemoryBackend())
self.auth_policy = auth_policy or AnonymousAuthPolicy()
self.jinja_env = self.configure_jinja(template_dirs, template_packages)
self.templating = templating.Jinja2Templates(env=self.jinja_env, context_processors=[self.template_context])
Expand All @@ -59,10 +59,10 @@ def __init__(

def configure_jinja(
self,
template_dirs: typing.Sequence[str | os.PathLike] = tuple(),
template_dirs: typing.Sequence[str | os.PathLike[str]] = tuple(),
template_packages: typing.Sequence[str] = tuple(),
) -> jinja2.Environment:
loaders = [jinja2.PackageLoader(PACKAGE_NAME)]
loaders: list[jinja2.BaseLoader] = [jinja2.PackageLoader(PACKAGE_NAME)]
loaders.append(jinja2.FileSystemLoader(list(template_dirs)))
loaders.extend([jinja2.PackageLoader(template_package) for template_package in template_packages])

Expand Down Expand Up @@ -108,10 +108,12 @@ async def login_view(self, request: Request) -> Response:
form_class = self.auth_policy.get_login_form_class()
form = form_class(formdata=await request.form(), data={"next_url": next_url})
if request.method in ["POST"] and form.validate():
if user := await self.auth_policy.authenticate(request, form.identity.data, form.password.data):
identity = typing.cast(str, form.identity.data)
password = typing.cast(str, form.password.data)
if user := await self.auth_policy.authenticate(request, identity, password):
self.auth_policy.login(request, user)
flash(request).success(_("You have been logged in.", domain="ohmyadmin"))
return RedirectResponse(url=form.next_url.data, status_code=302)
return RedirectResponse(url=typing.cast(str, form.next_url.data), status_code=302)
else:
flash(request).error(_("Invalid credentials.", domain="ohmyadmin"))

Expand Down
21 changes: 15 additions & 6 deletions ohmyadmin/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
import httpx
from bs4 import BeautifulSoup

T = typing.TypeVar("T")

class SelectorError(Exception):
...

class SelectorError(Exception): ...

class NodeNotFoundError(SelectorError):
...

class NodeNotFoundError(SelectorError): ...


class MarkupSelector:
Expand All @@ -29,8 +29,17 @@ def find_node_or_raise(self, selector: str) -> bs4.Tag:
return node
raise NodeNotFoundError(f'No nodes: "{selector}".')

def get_attribute(self, selector: str, attribute: str, default: typing.Any = None) -> str:
return self.find_node_or_raise(selector).get(attribute, default)
@typing.overload
def get_attribute(self, selector: str, attribute: str, default: str) -> str: ...

@typing.overload
def get_attribute(self, selector: str, attribute: str, default: None = None) -> None: ...

def get_attribute(self, selector: str, attribute: str, default: str | None = None) -> str | None:
value = self.find_node_or_raise(selector).get(attribute, default)
if not isinstance(value, str):
raise TypeError("Attribute must be a string.")
return value

def has_attribute(self, selector: str, attribute: str) -> bool:
return attribute in self.find_node_or_raise(selector).attrs
Expand Down
116 changes: 83 additions & 33 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ starception = "^1.0"
httpx = "^0.23.3"
ruff = "^0.1.7"
polyfactory = "^2.15.0"
types-beautifulsoup4 = "^4.12.0.20240229"
types-passlib = "^1.7.7.20240311"
types-wtforms = "^3.1.0.20240311"

[tool.poetry.group.docs.dependencies]
mkdocs = "^1.4.2"
Expand Down Expand Up @@ -81,7 +84,11 @@ exclude_lines = [
omit = [".venv/*", ".git/*", "*/__main__.py", "examples/*"]

[tool.mypy]
pretty = true
strict = true
show_error_codes = true
show_error_context = true
show_column_numbers = true
files = ["ohmyadmin", "examples"]

[tool.pytest.ini_options]
Expand Down
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ def app(app_f: AppFactory, ohmyadmin: OhMyAdmin) -> Starlette:


class RequestFactory(typing.Protocol): # pragma: no cover:
def __call__(self, method: str = "get", type: str = "http") -> Request:
...
def __call__(self, method: str = "get", type: str = "http") -> Request: ...


@pytest.fixture
Expand Down
6 changes: 2 additions & 4 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
from tests.models import User


def test_login_page() -> None:
...
def test_login_page() -> None: ...


def test_logout_page() -> None:
...
def test_logout_page() -> None: ...


async def test_auth_policy(http_get: Request, user: User) -> None:
Expand Down

0 comments on commit 3ef98d2

Please sign in to comment.