Skip to content

Commit 93f3f44

Browse files
committed
WIP
- Added more features
1 parent 46f1817 commit 93f3f44

37 files changed

+1752
-274
lines changed

.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DB_URI=""

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,6 @@ cython_debug/
159159
# and can be added to the global gitignore or merged into this file. For a more nuclear
160160
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
161161
#.idea/
162+
163+
.ruff_cache
164+
.vscode

cms.sqlite

68 KB
Binary file not shown.

main.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
"""FastAPI server."""
22

3-
from fastapi import FastAPI, Request, status
3+
import textwrap
4+
5+
from fastapi import FastAPI, status
46
from fastapi.middleware.cors import CORSMiddleware
57
from fastapi.responses import ORJSONResponse
6-
78
from src.core.config.api import APIConfig
89

9-
app = FastAPI(
10+
rosa = FastAPI(
1011
title=APIConfig.TITLE,
1112
version=APIConfig.VERSION,
12-
description=APIConfig.DESCRIPTION,
13+
summary=APIConfig.SUMMARY,
14+
description=textwrap.dedent(APIConfig.DESCRIPTION),
1315
default_response_class=ORJSONResponse,
14-
docs_url=f"/{APIConfig.VERSION}/docs",
15-
redoc_url=f"/{APIConfig.VERSION}/redoc",
16-
openapi_url=f"/{APIConfig.VERSION}/openapi.json",
16+
root_path=f"/{APIConfig.VERSION}",
17+
redirect_slashes=True,
1718
)
1819

19-
app.add_middleware(
20+
rosa.add_middleware(
2021
middleware_class=CORSMiddleware,
2122
allow_origins=APIConfig.CORS_ORIGINS,
2223
)
2324

2425

25-
@app.get(
26-
f"/{APIConfig.VERSION}/",
26+
@rosa.get(
27+
"/",
2728
status_code=status.HTTP_200_OK,
2829
response_class=ORJSONResponse,
2930
)
30-
def home(request: Request) -> ORJSONResponse:
31+
def home() -> ORJSONResponse:
3132
"""Home route."""
3233
return ORJSONResponse(content={"content": "successful response."})
34+
35+
36+
from src.cms.api import article, author, category
37+
38+
rosa.include_router(router=article)
39+
rosa.include_router(router=author)
40+
rosa.include_router(router=category)

poetry.lock

+314-229
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+19-27
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,30 @@ description = "Rosa is a simple and fast content management system focussed on m
55
authors = ["Pratheesh Prakash <[email protected]>"]
66
license = "GPL-3.0-or-later"
77
readme = "README.md"
8+
package-mode = false
89

910
[tool.poetry.dependencies]
10-
python = "^3.10"
11-
fastapi = "^0.110.3"
12-
uvicorn = { extras = ["standard"], version = "^0.29.0" }
13-
orjson = "^3.10.2"
14-
pydantic-settings = "^2.2.1"
15-
email-validator = "^2.1.1"
11+
python = "^3.12"
12+
fastapi = "^0.112.1"
13+
uvicorn = { extras = ["standard"], version = "0.30.6" }
14+
orjson = "^3.10.7"
15+
pydantic-settings = "^2.4.0"
16+
email-validator = "^2.2.0"
1617
httpx = "^0.27.0"
1718
python-multipart = "^0.0.9"
1819
itsdangerous = "^2.2.0"
19-
pyyaml = "^6.0.1"
20-
strawberry-graphql = { extras = ["fastapi"], version = "^0.227.2" }
21-
pytest = "^8.2.0"
22-
sqlalchemy = "^2.0.29"
23-
ruff = "^0.4.2"
20+
pyyaml = "^6.0.2"
21+
strawberry-graphql = "^0.237.3"
22+
pytest = "^8.3.2"
23+
sqlalchemy = {extras = ["asyncio"], version = "2.0.32"}
24+
ruff = "^0.6.1"
2425
refurb = "^2.0.0"
2526
pydocstyle = "^6.3.0"
2627
isort = "^5.13.2"
28+
asyncio = "^3.4.3"
29+
aiosqlite = "^0.20.0"
30+
asyncpg = "^0.29.0"
31+
pytest-asyncio = "^0.23.8"
2732

2833

2934
[build-system]
@@ -42,6 +47,7 @@ warn_required_dynamic_aliases = true
4247
warn_untyped_fields = false
4348

4449
[tool.ruff]
50+
src = ["src"]
4551
target-version = "py312"
4652
line-length = 100
4753
exclude = [
@@ -59,6 +65,7 @@ exclude = [
5965
".pytest_cache",
6066
".pytype",
6167
".ruff_cache",
68+
".env.example",
6269
".svn",
6370
".tox",
6471
".venv",
@@ -83,22 +90,7 @@ line-ending = "auto"
8390

8491
[tool.ruff.lint]
8592

86-
select = [
87-
"E",
88-
"F",
89-
"C90",
90-
"W",
91-
"I",
92-
"D",
93-
"UP",
94-
"ERA",
95-
"PD",
96-
"PL",
97-
"TRY",
98-
"NPY",
99-
"FURB",
100-
"RUF",
101-
]
93+
select = ["ALL"]
10294

10395
ignore = ["UP038", "D104"]
10496

src/authentication/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .user import User

src/authentication/models/user.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""User model."""
2+
3+
from sqlalchemy import ForeignKey
4+
from sqlalchemy.orm import Mapped, mapped_column
5+
from sqlalchemy.types import INTEGER, TEXT
6+
from src.core.database.db import Base
7+
8+
9+
class User(Base):
10+
"""User schema."""
11+
12+
__tablename__: str = "user"
13+
14+
id: Mapped[int] = mapped_column(
15+
name="id",
16+
primary_key=True,
17+
autoincrement="auto",
18+
type_=INTEGER,
19+
)
20+
name: Mapped[str] = mapped_column(
21+
name="username",
22+
type_=TEXT,
23+
nullable=False,
24+
unique=True,
25+
)
26+
27+
email: Mapped[str] = mapped_column(
28+
name="email",
29+
type_=TEXT,
30+
nullable=False,
31+
unique=True,
32+
)
33+
34+
is_superuser: Mapped[bool] = mapped_column(default=False)
35+
36+
password_hash: Mapped[str] = mapped_column(
37+
name="password_hash",
38+
type_=TEXT,
39+
nullable=False,
40+
)
41+
42+
published_article_id: Mapped[int] = mapped_column(
43+
ForeignKey("article.id"),
44+
type_=INTEGER,
45+
nullable=True,
46+
)

src/cms/api/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""API routes."""
2+
3+
from .article import article
4+
from .author import author
5+
from .category import category
6+
7+
__all__: list[str] = [
8+
"article",
9+
"author",
10+
"category",
11+
]

src/cms/api/article.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Article routes."""
2+
3+
from fastapi import APIRouter, Query, status
4+
from fastapi.responses import ORJSONResponse
5+
from src.cms.services.articles import fetch_latest_articles_from_db
6+
from src.core.config.api import ArticleConfig
7+
8+
article = APIRouter(
9+
prefix="/article",
10+
tags=["article"],
11+
default_response_class=ORJSONResponse,
12+
)
13+
14+
15+
@article.get(
16+
path="/",
17+
response_class=ORJSONResponse,
18+
tags=["article"],
19+
summary="List all articles in reverse chronological order.",
20+
description="Get the list of all articles ordered in reverse chronological order.",
21+
deprecated=False,
22+
name="Article",
23+
status_code=status.HTTP_200_OK,
24+
)
25+
def fetch_articles(
26+
fetch_count: int = Query(
27+
default=ArticleConfig.DEFAULT_FETCHABLE_ARTICLES,
28+
alias="fetch_count",
29+
le=ArticleConfig.MAX_FETCHABLE_ARTICLES,
30+
ge=ArticleConfig.MIN_FETCHABLE_ARTICLES,
31+
),
32+
) -> ORJSONResponse:
33+
"""
34+
Fetch articles and return.
35+
36+
Parameters
37+
----------
38+
fetch_count : int, optional
39+
Number of articles to fetch, by default 10.
40+
41+
Returns
42+
-------
43+
ORJSONResponse
44+
Returns list of articles in reverse chronological order.
45+
"""
46+
if fetch_count > ArticleConfig.MAX_FETCHABLE_ARTICLES:
47+
fetch_count = ArticleConfig.MAX_FETCHABLE_ARTICLES
48+
elif fetch_count < ArticleConfig.MIN_FETCHABLE_ARTICLES:
49+
fetch_count = ArticleConfig.MIN_FETCHABLE_ARTICLES
50+
51+
articles = fetch_latest_articles_from_db(n=fetch_count)
52+
return ORJSONResponse(content=articles)

src/cms/api/author.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Author routes."""
2+
3+
from fastapi import APIRouter, Query, status
4+
from fastapi.responses import ORJSONResponse
5+
from src.cms.services.authors import fetch_articles_by_author, fetch_authors_from_db
6+
from src.core.config.api import ArticleConfig
7+
8+
author = APIRouter(
9+
prefix="/author",
10+
tags=["author"],
11+
default_response_class=ORJSONResponse,
12+
)
13+
14+
15+
@author.get(
16+
path="/",
17+
response_class=ORJSONResponse,
18+
tags=["author"],
19+
summary="List all published authors.",
20+
description="List all published authors in the alphabetic order of their first name",
21+
deprecated=False,
22+
name="Author",
23+
status_code=status.HTTP_200_OK,
24+
)
25+
def fetch_authors(
26+
fetch_count: int = Query(
27+
default=ArticleConfig.DEFAULT_FETCHABLE_ARTICLES,
28+
alias="fetch_count",
29+
le=ArticleConfig.MAX_FETCHABLE_ARTICLES,
30+
ge=ArticleConfig.MIN_FETCHABLE_ARTICLES,
31+
),
32+
) -> ORJSONResponse:
33+
"""
34+
Fetch authors and return.
35+
36+
Parameters
37+
----------
38+
fetch_count : int, optional
39+
Number of authors to fetch, by default 10.
40+
41+
Returns
42+
-------
43+
ORJSONResponse
44+
Returns list of authors in alphabetical order.
45+
"""
46+
if fetch_count > ArticleConfig.MAX_FETCHABLE_ARTICLES:
47+
fetch_count = ArticleConfig.MAX_FETCHABLE_ARTICLES
48+
elif fetch_count < ArticleConfig.MIN_FETCHABLE_ARTICLES:
49+
fetch_count = ArticleConfig.MIN_FETCHABLE_ARTICLES
50+
51+
authors = fetch_authors_from_db()
52+
return ORJSONResponse(content=authors)
53+
54+
55+
@author.get(
56+
path="/{author_slug}/",
57+
response_class=ORJSONResponse,
58+
tags=["author", "article"],
59+
summary="List all articles in reverse chronological order.",
60+
description="Get the list of all articles ordered in reverse chronological order.",
61+
deprecated=False,
62+
name="Category-wise articles",
63+
status_code=status.HTTP_200_OK,
64+
)
65+
def fetch_articles(author_slug: str) -> ORJSONResponse:
66+
"""
67+
Fetch articles by author and return.
68+
69+
Parameters
70+
----------
71+
fetch_count : int, optional
72+
Number of articles to fetch, by default 10.
73+
74+
Returns
75+
-------
76+
ORJSONResponse
77+
Returns list of articles in reverse chronological order.
78+
"""
79+
articles = fetch_articles_by_author(author_slug=author_slug)
80+
return ORJSONResponse(content=articles)

0 commit comments

Comments
 (0)