repair tests

This commit is contained in:
L-Nafaryus 2024-08-30 12:38:43 +05:00
parent 3637ea99a8
commit 1b1142a0b0
Signed by: L-Nafaryus
GPG Key ID: 553C97999B363D38
6 changed files with 113 additions and 115 deletions

View File

@ -29,123 +29,114 @@ class ApplicationError(Exception):
class Application: class Application:
__instance__: Optional[Self] = None
def __init__( def __init__(
self, self,
config: Config, config: Config,
logger: LoggerInstance,
database: Database,
cache: Cache,
cron: Cron,
backend: FastAPI,
): ):
if Application.__instance__: self.config: Config = config
raise ApplicationError("Cannot create multiple applications") self.logger: Optional[LoggerInstance] = None
self.database: Optional[Database] = None
self.cache: Optional[Cache] = None
self.cron: Optional[Cron] = None
self.backend: Optional[FastAPI] = None
self.config = config self.prepare_logger()
self.logger = logger
self.database = database
self.cache = cache
self.cron = cron
self.backend = backend
@staticmethod @staticmethod
async def new(config: Config): async def new(config: Config):
if Application.__instance__: app = Application(config)
raise ApplicationError("Cannot create multiple applications")
logger = Logger.new(**config.log.model_dump())
# if user := config.application.user: # if user := config.application.user:
# os.setuid(pwd.getpwnam(user).pw_uid) # os.setuid(pwd.getpwnam(user).pw_uid)
# if group := config.application.group: # if group := config.application.group:
# os.setgid(pwd.getpwnam(user).pw_gid) # os.setgid(pwd.getpwnam(user).pw_gid)
logger.debug("Initializing application...") app.logger.debug("Initializing application...")
await app.prepare_working_directory()
try: try:
logger.debug("Changing working directory") await app.prepare_database()
os.chdir(config.application.working_directory.resolve()) await app.prepare_cache()
except FileNotFoundError as e: await app.prepare_cron()
logger.error("Failed to change working directory: {}", e) await app.prepare_server()
sys.exit()
try:
logger.info("Connecting to database {}", config.database.url())
database = await Database.new(config.database.url()) # type: ignore
logger.info("Connecting to cache server {}", config.cache.url())
cache = await Cache.new(config.cache.url()) # type: ignore
logger.info("Prepairing cron")
cron = Cron.new(
config.cron.workers_count,
backend_url=config.cache.url(),
broker_url=config.cache.url(),
)
logger.info("Running database migrations")
await database.run_migrations()
except Exception as e: except Exception as e:
logger.error(" ".join(e.args)) app.logger.error(" ".join(e.args))
sys.exit() sys.exit()
try: try:
import materia_frontend import materia_frontend
except ModuleNotFoundError: except ModuleNotFoundError:
logger.warning( app.logger.warning(
"`materia_frontend` is not installed. No user interface will be served." "`materia_frontend` is not installed. No user interface will be served."
) )
return app
def prepare_logger(self):
self.logger = Logger.new(**self.config.log.model_dump())
async def prepare_working_directory(self):
try:
self.logger.debug("Changing working directory")
os.chdir(self.config.application.working_directory.resolve())
except FileNotFoundError as e:
self.logger.error("Failed to change working directory: {}", e)
sys.exit()
async def prepare_database(self):
url = self.config.database.url()
self.logger.info("Connecting to database {}", url)
self.database = await Database.new(url) # type: ignore
async def prepare_cache(self):
url = self.config.cache.url()
self.logger.info("Connecting to cache server {}", url)
self.cache = await Cache.new(url) # type: ignore
async def prepare_cron(self):
url = self.config.cache.url()
self.logger.info("Prepairing cron")
self.cron = Cron.new(
self.config.cron.workers_count, backend_url=url, broker_url=url
)
async def prepare_server(self):
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[Context]: async def lifespan(app: FastAPI) -> AsyncIterator[Context]:
yield Context(config=config, logger=logger, database=database, cache=cache) yield Context(
config=self.config,
logger=self.logger,
database=self.database,
cache=self.cache,
)
if database.engine is not None: if self.database.engine is not None:
await database.dispose() await self.database.dispose()
backend = FastAPI( self.backend = FastAPI(
title="materia", title="materia",
version="0.1.0", version="0.1.0",
docs_url="/api/docs", docs_url="/api/docs",
lifespan=lifespan, lifespan=lifespan,
) )
backend.add_middleware( self.backend.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://localhost", "http://localhost:5173"], allow_origins=["http://localhost", "http://localhost:5173"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
backend.include_router(routers.api.router) self.backend.include_router(routers.api.router)
backend.include_router(routers.resources.router) self.backend.include_router(routers.resources.router)
backend.include_router(routers.root.router) self.backend.include_router(routers.root.router)
return Application(
config=config,
logger=logger,
database=database,
cache=cache,
cron=cron,
backend=backend,
)
@staticmethod
def instance() -> Optional[Self]:
return Application.__instance__
async def start(self): async def start(self):
self.logger.info(f"Spinning up cron workers [{self.config.cron.workers_count}]") self.logger.info(f"Spinning up cron workers [{self.config.cron.workers_count}]")
self.cron.run_workers() self.cron.run_workers()
try: try:
# uvicorn.run( self.logger.info("Running database migrations")
# self.backend, await self.database.run_migrations()
# port=self.config.server.port,
# host=str(self.config.server.address),
# # reload = config.application.mode == "development",
# log_config=Logger.uvicorn_config(self.config.log.level),
# )
uvicorn_config = uvicorn.Config( uvicorn_config = uvicorn.Config(
self.backend, self.backend,
port=self.config.server.port, port=self.config.server.port,
@ -157,3 +148,7 @@ class Application:
await server.serve() await server.serve()
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
self.logger.info("Exiting...") self.logger.info("Exiting...")
sys.exit()
except Exception as e:
self.logger.error(" ".join(e.args))
sys.exit()

View File

@ -95,19 +95,19 @@ class Repository(Base):
await session.refresh(user, attribute_names=["repository"]) await session.refresh(user, attribute_names=["repository"])
return user.repository return user.repository
async def used(self, session: SessionContext) -> int: async def used_capacity(self, session: SessionContext) -> int:
session.add(self) session.add(self)
await session.refresh(self, attribute_names=["files"]) await session.refresh(self, attribute_names=["files"])
return sum([file.size for file in self.files]) return sum([file.size for file in self.files])
async def remaining_capacity(self, session: SessionContext) -> int: async def remaining_capacity(self, session: SessionContext) -> int:
used = await self.used(session) used = await self.used_capacity(session)
return self.capacity - used return self.capacity - used
async def info(self, session: SessionContext) -> "RepositoryInfo": async def info(self, session: SessionContext) -> "RepositoryInfo":
info = RepositoryInfo.model_validate(self) info = RepositoryInfo.model_validate(self)
info.used = await self.used(session) info.used = await self.used_capacity(session)
return info return info

View File

@ -74,8 +74,7 @@ async def create(
repository: Repository = Depends(middleware.repository), repository: Repository = Depends(middleware.repository),
ctx: middleware.Context = Depends(), ctx: middleware.Context = Depends(),
): ):
database = await Database.new(ctx.config.database.url(), test_connection=False) async with ctx.database.session() as session:
async with database.session() as session:
capacity = await repository.remaining_capacity(session) capacity = await repository.remaining_capacity(session)
try: try:
@ -116,7 +115,7 @@ async def create(
file.remove() file.remove()
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path") raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Invalid path")
async with database.session() as session: async with ctx.database.session() as session:
target_directory = await validate_target_directory( target_directory = await validate_target_directory(
path, repository, session, ctx.config path, repository, session, ctx.config
) )

View File

@ -1,27 +1,17 @@
import pytest_asyncio import pytest_asyncio
from materia.config import Config
from materia.models import ( from materia.models import (
Database,
Cache,
User, User,
LoginType, LoginType,
) )
from materia.models.base import Base from materia.models.base import Base
from materia import security from materia import security
from materia.app import Application
from materia.core import Config, Database, Cache, Cron
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from materia.app import make_application, AppContext
from materia._logging import make_logger
from httpx import AsyncClient, ASGITransport, Cookies from httpx import AsyncClient, ASGITransport, Cookies
import asyncio
from fastapi import FastAPI
from contextlib import asynccontextmanager
from typing import AsyncIterator
from asgi_lifespan import LifespanManager from asgi_lifespan import LifespanManager
from fastapi.middleware.cors import CORSMiddleware
from materia import routers
from pathlib import Path from pathlib import Path
from copy import deepcopy
@pytest_asyncio.fixture(scope="session") @pytest_asyncio.fixture(scope="session")
@ -75,6 +65,17 @@ async def cache(config: Config) -> Cache:
yield cache_pytest yield cache_pytest
@pytest_asyncio.fixture(scope="session")
async def cron(config: Config) -> Cache:
cron_pytest = Cron.new(
config.cron.workers_count,
backend_url=config.cache.url(),
broker_url=config.cache.url(),
)
yield cron_pytest
@pytest_asyncio.fixture(scope="function", autouse=True) @pytest_asyncio.fixture(scope="function", autouse=True)
async def setup_database(database: Database): async def setup_database(database: Database):
async with database.connection() as connection: async with database.connection() as connection:
@ -121,30 +122,36 @@ async def api_config(config: Config, tmpdir) -> Config:
@pytest_asyncio.fixture(scope="function") @pytest_asyncio.fixture(scope="function")
async def api_client( async def api_client(
api_config: Config, database: Database, cache: Cache api_config: Config, database: Database, cache: Cache, cron: Cron
) -> AsyncClient: ) -> AsyncClient:
logger = make_logger(api_config) app = Application(api_config)
app.database = database
app.cache = cache
app.cron = cron
await app.prepare_server()
@asynccontextmanager # logger = make_logger(api_config)
async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]:
yield AppContext(
config=api_config, database=database, cache=cache, logger=logger
)
app = FastAPI(lifespan=lifespan) # @asynccontextmanager
app.include_router(routers.api.router) # async def lifespan(app: FastAPI) -> AsyncIterator[AppContext]:
app.include_router(routers.resources.router) # yield AppContext(
app.include_router(routers.root.router) # config=api_config, database=database, cache=cache, logger=logger
app.add_middleware( # )
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
async with LifespanManager(app) as manager: # app = FastAPI(lifespan=lifespan)
# app.include_router(routers.api.router)
# app.include_router(routers.resources.router)
# app.include_router(routers.root.router)
# app.add_middleware(
# CORSMiddleware,
# allow_origins=["*"],
# allow_credentials=True,
# allow_methods=["*"],
# allow_headers=["*"],
# )
async with LifespanManager(app.backend) as manager:
async with AsyncClient( async with AsyncClient(
transport=ASGITransport(app=manager.app), base_url=api_config.server.url() transport=ASGITransport(app=manager.app), base_url=api_config.server.url()
) as client: ) as client:

View File

@ -1,8 +1,6 @@
import pytest import pytest
from materia.config import Config from materia.core import Config
from httpx import AsyncClient, Cookies from httpx import AsyncClient, Cookies
from materia.models.base import Base
import aiofiles
from io import BytesIO from io import BytesIO
# TODO: replace downloadable images for tests # TODO: replace downloadable images for tests
@ -188,6 +186,6 @@ async def test_file(auth_client: AsyncClient, api_config: Config):
pytest_logo = BytesIO(pytest_logo_res.content) pytest_logo = BytesIO(pytest_logo_res.content)
create = await auth_client.post( create = await auth_client.post(
"/api/file", files={"file": ("pytest.png", pytest_logo)}, json={"path", "/"} "/api/file", files={"file": ("pytest.png", pytest_logo)}, data={"path": "/"}
) )
assert create.status_code == 200, create.text assert create.status_code == 200, create.text

View File

@ -1,7 +1,6 @@
import pytest_asyncio import pytest_asyncio
import pytest import pytest
from pathlib import Path from pathlib import Path
from materia.config import Config
from materia.models import ( from materia.models import (
User, User,
Repository, Repository,
@ -9,7 +8,7 @@ from materia.models import (
RepositoryError, RepositoryError,
File, File,
) )
from materia.models.database import SessionContext from materia.core import Config, SessionContext
from materia import security from materia import security
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.orm.session import make_transient from sqlalchemy.orm.session import make_transient