diff --git a/.gitignore b/.gitignore index 505a3b1..8b5d307 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,12 @@ wheels/ # Virtual environments .venv +.env + +# Log files +*.log +*.gz + + +# Data logs directory +src/data/logs/* \ No newline at end of file diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..e74efed --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1,4 @@ +from .logger import logger +from .settings import settings + +__all__ = ["logger", "settings"] diff --git a/src/lib/exceptions.py b/src/lib/exceptions.py new file mode 100644 index 0000000..b706af2 --- /dev/null +++ b/src/lib/exceptions.py @@ -0,0 +1,9 @@ +from fastapi import HTTPException, status + + +class NotImplementedException(HTTPException): + def __init__(self): + super().__init__( + status_code=status.HTTP_501_NOT_IMPLEMENTED, + detail="Not implemented yet", + ) diff --git a/src/lib/logger.py b/src/lib/logger.py new file mode 100644 index 0000000..71e37a8 --- /dev/null +++ b/src/lib/logger.py @@ -0,0 +1,24 @@ +from loguru import logger +from sys import stdout + +from .settings import settings + +logger.remove() + +logger.add( + stdout, + format="{time:YYYY-MM-DD at HH:mm:ss} | {file}:{line} | {level} | {message} | {extra}", + colorize=True, + enqueue=True, +) + +logger.add( + settings.log_dir / "file.log", + format="", + serialize=True, + level="INFO", + rotation="10 MB", + retention="1 week", + enqueue=True, + compression="gz", +) diff --git a/src/lib/settings.py b/src/lib/settings.py new file mode 100644 index 0000000..7d742c7 --- /dev/null +++ b/src/lib/settings.py @@ -0,0 +1,17 @@ +from pydantic_settings import BaseSettings +from pydantic import Field +from pathlib import Path + + +class Settings(BaseSettings): + data_dir: Path = Field(default=Path(__file__).parent.parent / "data") + + @property + def log_dir(self) -> Path: + dir = self.data_dir / "logs" + if not dir.is_dir(): + dir.mkdir(parents=True, exist_ok=True) + return dir + + +settings = Settings() diff --git a/src/main.py b/src/main.py index e69de29..3a749c4 100644 --- a/src/main.py +++ b/src/main.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from contextlib import asynccontextmanager + +from routes import routes +from lib import logger + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup code here + logger.info("Application startup") + yield + # Shutdown code here + logger.info("Application shutdown") + + +app = FastAPI(lifespan=lifespan) + + +for route in routes: + app.include_router(route) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/src/models/job_listing.py b/src/models/job_listing.py new file mode 100644 index 0000000..551f4c3 --- /dev/null +++ b/src/models/job_listing.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field + + +class JobListing(BaseModel): + title: str = Field(..., description="Job title") + company: str = Field(..., description="Company name") + location: str = Field(..., description="Location") + description: str = Field(..., description="Job description") + salary: int | None = Field(None, description="Salary") + url: str = Field(..., description="Job URL") diff --git a/src/models/message.py b/src/models/message.py new file mode 100644 index 0000000..d1fa357 --- /dev/null +++ b/src/models/message.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from zoneinfo import ZoneInfo +from enum import Enum + + +class _Type(Enum): + INFO = "info" + WARNING = "warning" + ERROR = "error" + SUCCESS = "success" + + +class _Message(BaseModel): + msg: str = Field(..., description="Message") + + timestamp: datetime = Field( + default_factory=lambda: datetime.now(tz=ZoneInfo("UTC")), + description="Timestamp", + ) + + +class Error(_Message): + msg_type: _Type = _Type.ERROR + + +class Warning(_Message): + msg_type: _Type = _Type.WARNING + + +class Info(_Message): + msg_type: _Type = _Type.INFO + + +class Success(_Message): + msg_type: _Type = _Type.SUCCESS diff --git a/src/routes/__init__.py b/src/routes/__init__.py new file mode 100644 index 0000000..b49acf8 --- /dev/null +++ b/src/routes/__init__.py @@ -0,0 +1,6 @@ +from .default import router as default_router +from .job_postings import router as job_postings_router + +routes = [default_router, job_postings_router] + +__all__ = ["routes"] diff --git a/src/routes/default.py b/src/routes/default.py new file mode 100644 index 0000000..bb08e4a --- /dev/null +++ b/src/routes/default.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter +from models.message import Success + +router = APIRouter(tags=["Default"]) + + +@router.get("/") +def root(): + return Success(msg="Hello World") + + +@router.get("/health") +def health(): + return Success(msg="ok") diff --git a/src/routes/job_postings.py b/src/routes/job_postings.py new file mode 100644 index 0000000..8c2aa49 --- /dev/null +++ b/src/routes/job_postings.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter +from enum import Enum + +from lib.exceptions import NotImplementedException +from lib import logger + +router = APIRouter(prefix="/job_postings", tags=["Job Postings"]) + + +class PostingSource(Enum): + ALL = "all" + LINKEDIN = "linkedin" + DUUNITORI = "duunitori" + TYOMARKKINATORI = "tyomarkkinatori" + + +@router.get("/") +def get_job_postings(source: PostingSource = PostingSource.ALL): + match source: + case PostingSource.ALL: + logger.error(f"Job postings from {source.value} Not implemented yet") + raise NotImplementedException() + case PostingSource.LINKEDIN: + logger.error(f"Job postings from {source.value} Not implemented yet") + raise NotImplementedException() + case PostingSource.DUUNITORI: + logger.error(f"Job postings from {source.value} Not implemented yet") + raise NotImplementedException() + case PostingSource.TYOMARKKINATORI: + logger.error(f"Job postings from {source.value} Not implemented yet") + raise NotImplementedException()