From 4f34251198f6e2cde964fa3077a821d4b92a16ce Mon Sep 17 00:00:00 2001 From: Thuvaraka Yogarajah Date: Mon, 6 Apr 2026 22:34:22 +0200 Subject: [PATCH] Add initial API backend scaffold --- API_ENDPOINTS.md | 75 ++++ KERNMODELL.md | 78 +++++ backend/__init__.py | 1 + backend/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 116 bytes backend/__pycache__/main.cpython-313.pyc | Bin 0 -> 21782 bytes backend/__pycache__/models.cpython-313.pyc | Bin 0 -> 2431 bytes backend/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 6703 bytes backend/__pycache__/store.cpython-313.pyc | Bin 0 -> 2786 bytes backend/main.py | 346 +++++++++++++++++++ backend/models.py | 65 ++++ backend/schemas.py | 137 ++++++++ backend/store.py | 103 ++++++ requirements.txt | 4 + 13 files changed, 809 insertions(+) create mode 100644 API_ENDPOINTS.md create mode 100644 KERNMODELL.md create mode 100644 backend/__init__.py create mode 100644 backend/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/__pycache__/main.cpython-313.pyc create mode 100644 backend/__pycache__/models.cpython-313.pyc create mode 100644 backend/__pycache__/schemas.cpython-313.pyc create mode 100644 backend/__pycache__/store.cpython-313.pyc create mode 100644 backend/main.py create mode 100644 backend/models.py create mode 100644 backend/schemas.py create mode 100644 backend/store.py create mode 100644 requirements.txt diff --git a/API_ENDPOINTS.md b/API_ENDPOINTS.md new file mode 100644 index 0000000..1d6e2e1 --- /dev/null +++ b/API_ENDPOINTS.md @@ -0,0 +1,75 @@ +# API-Endpunkte + +Diese Endpunkte decken die Kernfunktionen von OnlyPrompt ab. + +## Auth + +### `POST /api/v1/auth/register` +- Erstellt einen neuen Benutzer. + +### `POST /api/v1/auth/login` +- Loggt einen Benutzer ein. + +### `GET /api/v1/auth/me` +- Gibt den aktuell eingeloggten Benutzer zurueck. + +## User / Profile + +### `GET /api/v1/users/{id}` +- Ruft das oeffentliche Benutzerprofil anhand der ID ab. + +### `GET /api/v1/profile/me` +- Ruft das eigene Profil des eingeloggten Benutzers ab. + +### `PUT /api/v1/profile/me` +- Bearbeitet das eigene Profil des eingeloggten Benutzers. + +## Prompts + +### `GET /api/v1/prompts` +- Gibt alle veroeffentlichten Prompts zurueck. + +### `GET /api/v1/prompts/{id}` +- Ruft einen einzelnen Prompt anhand der ID ab. + +### `POST /api/v1/prompts` +- Erstellt einen neuen Prompt. + +### `PUT /api/v1/prompts/{id}` +- Bearbeitet einen bestehenden Prompt. + +### `DELETE /api/v1/prompts/{id}` +- Loescht einen Prompt. + +## Filter fuer Marketplace + +### `GET /api/v1/prompts?category=coding` +- Filtert Prompts nach Kategorie. + +### `GET /api/v1/prompts?search=python` +- Sucht Prompts ueber einen Suchbegriff. + +### `GET /api/v1/prompts?creatorId=5` +- Filtert Prompts nach einem bestimmten Creator. + +## Kategorien + +### `GET /api/v1/categories` +- Gibt alle verfuegbaren Kategorien zurueck. + +## Kaeufe + +### `POST /api/v1/purchases` +- Erstellt einen Kauf fuer einen Prompt. + +### `GET /api/v1/purchases/me` +- Gibt alle eigenen Kaeufe des eingeloggten Benutzers zurueck. + +## Reviews + +### `GET /api/v1/prompts/{id}/reviews` +- Ruft alle Bewertungen fuer einen Prompt ab. + +### `POST /api/v1/prompts/{id}/reviews` +- Erstellt eine neue Bewertung fuer einen Prompt. + diff --git a/KERNMODELL.md b/KERNMODELL.md new file mode 100644 index 0000000..7416f8d --- /dev/null +++ b/KERNMODELL.md @@ -0,0 +1,78 @@ +# Kernmodell + +Dieses Kernmodell beschreibt die wichtigsten Klassen, die OnlyPrompt fuer Login, Profile, Marketplace und Prompt-Kaeufe benoetigt. + +## Klassen + +### User +- `id: int` +- `username: string` +- `email: string` +- `passwordHash: string` +- `role: string` +- `createdAt: datetime` + +### Profile +- `id: int` +- `userId: int` +- `displayName: string` +- `bio: string` +- `avatarUrl: string` +- `specialties: string` + +### Prompt +- `id: int` +- `creatorId: int` +- `categoryId: int` +- `title: string` +- `description: string` +- `content: text` +- `price: decimal` +- `thumbnailUrl: string` +- `ratingAverage: float` +- `reviewCount: int` +- `status: string` +- `createdAt: datetime` +- `updatedAt: datetime` + +### Category +- `id: int` +- `name: string` +- `slug: string` + +### Purchase +- `id: int` +- `buyerId: int` +- `promptId: int` +- `pricePaid: decimal` +- `purchasedAt: datetime` + +### Review +- `id: int` +- `promptId: int` +- `userId: int` +- `rating: int` +- `comment: string` +- `createdAt: datetime` + +## Beziehungen + +- Ein `User` hat genau ein `Profile`. +- Ein `User` kann viele `Prompts` erstellen. +- Ein `Prompt` gehoert zu genau einer `Category`. +- Ein `User` kann viele `Prompts` ueber `Purchase` kaufen. +- Ein `User` kann viele `Reviews` schreiben. +- Ein `Prompt` kann viele `Reviews` erhalten. + +## UML-Kurzform + +```text +User 1 --- 1 Profile +User 1 --- * Prompt +Category 1 --- * Prompt +User 1 --- * Purchase +Prompt 1 --- * Purchase +User 1 --- * Review +Prompt 1 --- * Review +``` + diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/__pycache__/__init__.cpython-313.pyc b/backend/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ccdb4d37e501f8f1540ff995d7e0dab38e297ac8 GIT binary patch literal 116 zcmey&%ge<81P4T~WHJKj#~=<2fCNC`GYgQI%8<^W$>_I|p@<121QNd`oRpZHotl@T sA0MBYmst`YuUAlci^C>2KczG$)vkyYs0L(4F^KVznURsPh#ANN0Pgk{^8f$< literal 0 HcmV?d00001 diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d55f6043cbce7f023db0d730c39b0f0356c50a71 GIT binary patch literal 21782 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 SwiE04#JVB&GkisH@c#iAEQP`V literal 0 HcmV?d00001 diff --git a/backend/__pycache__/models.cpython-313.pyc b/backend/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e1e64a290f0c6ed450acacdf920cbf4ba5cdd0e GIT binary patch literal 2431 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 User: + user = store.users.get(store.current_user_id) + if user is None: + raise HTTPException(status_code=404, detail="Current user not found.") + return user + + +@app.post("/api/auth/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED) +def register(payload: RegisterRequest) -> AuthResponse: + if any(user.email == payload.email for user in store.users.values()): + raise HTTPException(status_code=400, detail="Email already exists.") + if any(user.username == payload.username for user in store.users.values()): + raise HTTPException(status_code=400, detail="Username already exists.") + + now = datetime.utcnow() + user_id = store.next_id("user") + user = User( + id=user_id, + email=payload.email, + password_hash=payload.password, + full_name=payload.full_name, + username=payload.username, + bio="", + location="", + avatar_url="", + role=payload.role, + is_verified=False, + created_at=now, + ) + store.users[user_id] = user + store.current_user_id = user_id + return AuthResponse(message="User created successfully.", user=UserResponse.model_validate(user)) + + +@app.post("/api/auth/login", response_model=AuthResponse) +def login(payload: LoginRequest) -> AuthResponse: + user = next((item for item in store.users.values() if item.email == payload.email), None) + if user is None or user.password_hash != payload.password: + raise HTTPException(status_code=401, detail="Invalid email or password.") + + store.current_user_id = user.id + return AuthResponse(message="Login successful.", user=UserResponse.model_validate(user)) + + +@app.get("/api/prompts", response_model=list[PromptResponse]) +def list_prompts( + category: str | None = Query(default=None), + search: str | None = Query(default=None), +) -> list[PromptResponse]: + prompts = list(store.prompts.values()) + + if category: + prompts = [prompt for prompt in prompts if prompt.category.lower() == category.lower()] + + if search: + term = search.lower() + prompts = [ + prompt + for prompt in prompts + if term in prompt.title.lower() or term in prompt.description.lower() + ] + + return [PromptResponse.model_validate(prompt) for prompt in prompts] + + +@app.get("/api/prompts/{prompt_id}", response_model=PromptResponse) +def get_prompt(prompt_id: int) -> PromptResponse: + prompt = store.prompts.get(prompt_id) + if prompt is None: + raise HTTPException(status_code=404, detail="Prompt not found.") + return PromptResponse.model_validate(prompt) + + +@app.post("/api/prompts", response_model=PromptResponse, status_code=status.HTTP_201_CREATED) +def create_prompt(payload: PromptCreateRequest) -> PromptResponse: + creator = store.users.get(payload.creator_id) + if creator is None or creator.role != "creator": + raise HTTPException(status_code=404, detail="Creator not found.") + + prompt = Prompt( + id=store.next_id("prompt"), + title=payload.title, + description=payload.description, + content=payload.content, + image_url=payload.image_url, + category=payload.category, + price=payload.price, + creator_id=payload.creator_id, + created_at=datetime.utcnow(), + ) + store.prompts[prompt.id] = prompt + return PromptResponse.model_validate(prompt) + + +@app.put("/api/prompts/{prompt_id}", response_model=PromptResponse) +def update_prompt(prompt_id: int, payload: PromptUpdateRequest) -> PromptResponse: + prompt = store.prompts.get(prompt_id) + if prompt is None: + raise HTTPException(status_code=404, detail="Prompt not found.") + + updates = payload.model_dump(exclude_unset=True) + for field, value in updates.items(): + setattr(prompt, field, value) + return PromptResponse.model_validate(prompt) + + +@app.delete("/api/prompts/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_prompt(prompt_id: int) -> Response: + if prompt_id not in store.prompts: + raise HTTPException(status_code=404, detail="Prompt not found.") + del store.prompts[prompt_id] + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@app.get("/api/creators", response_model=list[UserResponse]) +def list_creators(sort: str | None = Query(default=None)) -> list[UserResponse]: + creators = [user for user in store.users.values() if user.role == "creator"] + + if sort == "new": + creators.sort(key=lambda item: item.created_at, reverse=True) + elif sort == "popular": + creators.sort( + key=lambda item: sum(1 for follow in store.follows.values() if follow.creator_id == item.id), + reverse=True, + ) + elif sort == "top_rated": + def creator_rating(user: User) -> float: + creator_prompt_ids = [prompt.id for prompt in store.prompts.values() if prompt.creator_id == user.id] + ratings = [rating.score for rating in store.ratings.values() if rating.prompt_id in creator_prompt_ids] + return sum(ratings) / len(ratings) if ratings else 0 + + creators.sort(key=creator_rating, reverse=True) + + return [UserResponse.model_validate(creator) for creator in creators] + + +@app.get("/api/creators/{creator_id}", response_model=CreatorDetailResponse) +def get_creator(creator_id: int) -> CreatorDetailResponse: + creator = store.users.get(creator_id) + if creator is None or creator.role != "creator": + raise HTTPException(status_code=404, detail="Creator not found.") + + prompts = [prompt for prompt in store.prompts.values() if prompt.creator_id == creator_id] + return CreatorDetailResponse( + creator=UserResponse.model_validate(creator), + prompts=[PromptResponse.model_validate(prompt) for prompt in prompts], + ) + + +@app.get("/api/prompts/{prompt_id}/ratings", response_model=list[RatingResponse]) +def list_ratings(prompt_id: int) -> list[RatingResponse]: + if prompt_id not in store.prompts: + raise HTTPException(status_code=404, detail="Prompt not found.") + ratings = [rating for rating in store.ratings.values() if rating.prompt_id == prompt_id] + return [RatingResponse.model_validate(rating) for rating in ratings] + + +@app.post("/api/prompts/{prompt_id}/ratings", response_model=RatingResponse, status_code=status.HTTP_201_CREATED) +def create_rating(prompt_id: int, payload: RatingCreateRequest) -> RatingResponse: + if prompt_id not in store.prompts: + raise HTTPException(status_code=404, detail="Prompt not found.") + if payload.user_id not in store.users: + raise HTTPException(status_code=404, detail="User not found.") + + rating = Rating( + id=store.next_id("rating"), + prompt_id=prompt_id, + user_id=payload.user_id, + score=payload.score, + comment=payload.comment, + created_at=datetime.utcnow(), + ) + store.ratings[rating.id] = rating + return RatingResponse.model_validate(rating) + + +@app.get("/api/favorites", response_model=list[FavoriteResponse]) +def list_favorites() -> list[FavoriteResponse]: + current_user = get_current_user() + favorites = [item for item in store.favorites.values() if item.user_id == current_user.id] + return [FavoriteResponse.model_validate(item) for item in favorites] + + +@app.post("/api/favorites", response_model=FavoriteResponse, status_code=status.HTTP_201_CREATED) +def create_favorite(payload: FavoriteCreateRequest) -> FavoriteResponse: + current_user = get_current_user() + if payload.prompt_id not in store.prompts: + raise HTTPException(status_code=404, detail="Prompt not found.") + + existing = next( + ( + item + for item in store.favorites.values() + if item.user_id == current_user.id and item.prompt_id == payload.prompt_id + ), + None, + ) + if existing is not None: + return FavoriteResponse.model_validate(existing) + + favorite = Favorite( + id=store.next_id("favorite"), + user_id=current_user.id, + prompt_id=payload.prompt_id, + created_at=datetime.utcnow(), + ) + store.favorites[favorite.id] = favorite + return FavoriteResponse.model_validate(favorite) + + +@app.delete("/api/favorites/{prompt_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_favorite(prompt_id: int) -> Response: + current_user = get_current_user() + favorite_id = next( + ( + item.id + for item in store.favorites.values() + if item.user_id == current_user.id and item.prompt_id == prompt_id + ), + None, + ) + if favorite_id is None: + raise HTTPException(status_code=404, detail="Favorite not found.") + + del store.favorites[favorite_id] + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@app.post("/api/follows/{creator_id}", response_model=FollowResponse, status_code=status.HTTP_201_CREATED) +def create_follow(creator_id: int) -> FollowResponse: + current_user = get_current_user() + creator = store.users.get(creator_id) + if creator is None or creator.role != "creator": + raise HTTPException(status_code=404, detail="Creator not found.") + + existing = next( + ( + item + for item in store.follows.values() + if item.follower_id == current_user.id and item.creator_id == creator_id + ), + None, + ) + if existing is not None: + return FollowResponse.model_validate(existing) + + follow = Follow( + id=store.next_id("follow"), + follower_id=current_user.id, + creator_id=creator_id, + created_at=datetime.utcnow(), + ) + store.follows[follow.id] = follow + return FollowResponse.model_validate(follow) + + +@app.delete("/api/follows/{creator_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_follow(creator_id: int) -> Response: + current_user = get_current_user() + follow_id = next( + ( + item.id + for item in store.follows.values() + if item.follower_id == current_user.id and item.creator_id == creator_id + ), + None, + ) + if follow_id is None: + raise HTTPException(status_code=404, detail="Follow not found.") + + del store.follows[follow_id] + return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@app.get("/api/profile", response_model=UserResponse) +def get_profile() -> UserResponse: + return UserResponse.model_validate(get_current_user()) + + +@app.put("/api/profile", response_model=UserResponse) +def update_profile(payload: ProfileUpdateRequest) -> UserResponse: + user = get_current_user() + updates = payload.model_dump(exclude_unset=True) + for field, value in updates.items(): + setattr(user, field, value) + return UserResponse.model_validate(user) + + +@app.get("/api/chats/{user_id}", response_model=list[ChatMessageResponse]) +def get_chat(user_id: int) -> list[ChatMessageResponse]: + current_user = get_current_user() + if user_id not in store.users: + raise HTTPException(status_code=404, detail="User not found.") + + messages = [ + message + for message in store.chat_messages.values() + if {message.sender_id, message.receiver_id} == {current_user.id, user_id} + ] + messages.sort(key=lambda item: item.created_at) + return [ChatMessageResponse.model_validate(message) for message in messages] + + +@app.post("/api/chats", response_model=ChatMessageResponse, status_code=status.HTTP_201_CREATED) +def create_chat_message(payload: ChatMessageCreateRequest) -> ChatMessageResponse: + current_user = get_current_user() + if payload.receiver_id not in store.users: + raise HTTPException(status_code=404, detail="Receiver not found.") + + message = ChatMessage( + id=store.next_id("chat_message"), + sender_id=current_user.id, + receiver_id=payload.receiver_id, + content=payload.content, + created_at=datetime.utcnow(), + ) + store.chat_messages[message.id] = message + return ChatMessageResponse.model_validate(message) diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..d3ca0e3 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +from datetime import datetime + + +@dataclass +class User: + id: int + email: str + password_hash: str + full_name: str + username: str + bio: str + location: str + avatar_url: str + role: str + is_verified: bool + created_at: datetime + + +@dataclass +class Prompt: + id: int + title: str + description: str + content: str + image_url: str + category: str + price: float + creator_id: int + created_at: datetime + + +@dataclass +class Rating: + id: int + prompt_id: int + user_id: int + score: int + comment: str + created_at: datetime + + +@dataclass +class Favorite: + id: int + user_id: int + prompt_id: int + created_at: datetime + + +@dataclass +class Follow: + id: int + follower_id: int + creator_id: int + created_at: datetime + + +@dataclass +class ChatMessage: + id: int + sender_id: int + receiver_id: int + content: str + created_at: datetime diff --git a/backend/schemas.py b/backend/schemas.py new file mode 100644 index 0000000..53ee599 --- /dev/null +++ b/backend/schemas.py @@ -0,0 +1,137 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, EmailStr, Field + + +class UserResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + email: EmailStr + full_name: str + username: str + bio: str + location: str + avatar_url: str + role: str + is_verified: bool + created_at: datetime + + +class RegisterRequest(BaseModel): + email: EmailStr + password: str = Field(min_length=6) + full_name: str = Field(min_length=2, max_length=100) + username: str = Field(min_length=3, max_length=50) + role: str = Field(default="user") + + +class LoginRequest(BaseModel): + email: EmailStr + password: str = Field(min_length=6) + + +class AuthResponse(BaseModel): + message: str + user: UserResponse + + +class ProfileUpdateRequest(BaseModel): + full_name: Optional[str] = Field(default=None, min_length=2, max_length=100) + bio: Optional[str] = Field(default=None, max_length=500) + location: Optional[str] = Field(default=None, max_length=120) + avatar_url: Optional[str] = Field(default=None, max_length=255) + is_verified: Optional[bool] = None + + +class PromptCreateRequest(BaseModel): + title: str = Field(min_length=3, max_length=120) + description: str = Field(min_length=10, max_length=500) + content: str = Field(min_length=10) + image_url: str = Field(max_length=255) + category: str = Field(min_length=2, max_length=50) + price: float = Field(ge=0) + creator_id: int + + +class PromptUpdateRequest(BaseModel): + title: Optional[str] = Field(default=None, min_length=3, max_length=120) + description: Optional[str] = Field(default=None, min_length=10, max_length=500) + content: Optional[str] = Field(default=None, min_length=10) + image_url: Optional[str] = Field(default=None, max_length=255) + category: Optional[str] = Field(default=None, min_length=2, max_length=50) + price: Optional[float] = Field(default=None, ge=0) + + +class PromptResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + title: str + description: str + content: str + image_url: str + category: str + price: float + creator_id: int + created_at: datetime + + +class CreatorDetailResponse(BaseModel): + creator: UserResponse + prompts: list[PromptResponse] + + +class RatingCreateRequest(BaseModel): + user_id: int + score: int = Field(ge=1, le=5) + comment: str = Field(default="", max_length=500) + + +class RatingResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + prompt_id: int + user_id: int + score: int + comment: str + created_at: datetime + + +class FavoriteCreateRequest(BaseModel): + prompt_id: int + + +class FavoriteResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + user_id: int + prompt_id: int + created_at: datetime + + +class FollowResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + follower_id: int + creator_id: int + created_at: datetime + + +class ChatMessageCreateRequest(BaseModel): + receiver_id: int + content: str = Field(min_length=1, max_length=2000) + + +class ChatMessageResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + sender_id: int + receiver_id: int + content: str + created_at: datetime diff --git a/backend/store.py b/backend/store.py new file mode 100644 index 0000000..fe5764e --- /dev/null +++ b/backend/store.py @@ -0,0 +1,103 @@ +from datetime import datetime + +from .models import ChatMessage, Favorite, Follow, Prompt, Rating, User + + +class InMemoryStore: + def __init__(self) -> None: + now = datetime.utcnow() + + self.users = { + 1: User( + id=1, + email="jane@example.com", + password_hash="demo-password", + full_name="Jane Doe", + username="janedoe", + bio="AI creator focused on writing and coding prompts.", + location="Zurich", + avatar_url="/images/content/creator1.png", + role="creator", + is_verified=True, + created_at=now, + ), + 2: User( + id=2, + email="max@example.com", + password_hash="demo-password", + full_name="Max Muster", + username="maxm", + bio="Prompt buyer and tester.", + location="Chur", + avatar_url="/images/content/creator2.png", + role="user", + is_verified=False, + created_at=now, + ), + } + self.prompts = { + 1: Prompt( + id=1, + title="Python Code Assistant", + description="Efficiently debug and write Python code with AI assistance.", + content="You are an expert Python assistant...", + image_url="/images/content/prompt2.png", + category="Coding", + price=19.99, + creator_id=1, + created_at=now, + ) + } + self.ratings = { + 1: Rating( + id=1, + prompt_id=1, + user_id=2, + score=5, + comment="Very useful starter prompt.", + created_at=now, + ) + } + self.favorites = { + 1: Favorite( + id=1, + user_id=2, + prompt_id=1, + created_at=now, + ) + } + self.follows = { + 1: Follow( + id=1, + follower_id=2, + creator_id=1, + created_at=now, + ) + } + self.chat_messages = { + 1: ChatMessage( + id=1, + sender_id=2, + receiver_id=1, + content="Hi, I have a question about your prompt.", + created_at=now, + ) + } + + self.current_user_id = 2 + self.next_ids = { + "user": 3, + "prompt": 2, + "rating": 2, + "favorite": 2, + "follow": 2, + "chat_message": 2, + } + + def next_id(self, key: str) -> int: + value = self.next_ids[key] + self.next_ids[key] += 1 + return value + + +store = InMemoryStore() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..59b441a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn +pydantic +email-validator