Skip to content

Commit

Permalink
move server to monorepo
Browse files Browse the repository at this point in the history
  • Loading branch information
EthanBonsignori committed Feb 19, 2024
1 parent 7c7da9a commit 803e534
Show file tree
Hide file tree
Showing 30 changed files with 868 additions and 29 deletions.
28 changes: 28 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Client
node_modules
dist
.env
.env.production

# Server
__pycache__
.venv
.DS_STORE
poetry.lock
*.log
.ignore

# temp
cloudbuild.yaml
docker-compose.yaml
Makefile
start.sh
7 changes: 5 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
// The following will hide the js and map files in the editor
"files.exclude": {
"**/__pycache__": true
},
"files.trimTrailingWhitespace": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
},
"cSpell.words": ["tanstack"]
}
}
File renamed without changes.
27 changes: 0 additions & 27 deletions client/.gitignore

This file was deleted.

9 changes: 9 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
LOG_LEVEL=WARN
ENV=development

# MongoDB Config
MONGO_DB_NAME=db
ATLAS_USERNAME=admin
ATLAS_PASSWORD=password
MAX_CONNECTIONS_COUNT=100
MIN_CONNECTIONS_COUNT=0
14 changes: 14 additions & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.12

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --upgrade pip
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY . /code

EXPOSE 80

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
25 changes: 25 additions & 0 deletions server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# uservote-api

## Description

FastAPI based API for uservote project.

## Requirements

Python 3.8+

## Installation

```bash
pip install -r requirements.txt
```

## Usage

```bash
bash start.sh
```

## License

MIT
Empty file added server/app/api/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions server/app/api/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from motor.core import AgnosticDatabase
import platform
import psutil

from app.database.database import get_database_client

router = APIRouter()


@router.get('/', include_in_schema=False)
@router.get('')
async def health(db: AgnosticDatabase = Depends(get_database_client)):
try:
# Check if the database is responsive
await db.command('ping')
db_status = 'up'
except Exception:
db_status = 'down'

# Get system information
system_info = {
"system": platform.system(),
"processor": platform.processor(),
"architecture": platform.architecture(),
"memory": psutil.virtual_memory()._asdict(),
"disk": psutil.disk_usage('/')._asdict()
}

return JSONResponse(
status_code=db_status == 'up' and 200 or 503,
content={
"database": db_status,
"system_info": system_info
}
)
Empty file added server/app/api/v1/__init__.py
Empty file.
122 changes: 122 additions & 0 deletions server/app/api/v1/feature_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from fastapi import APIRouter, Depends, Response
from uuid import UUID

import logging
from app.database.database import get_database_client, AgnosticDatabase
from app.common.utils import uuid_masker
from app.common.error import UnprocessableError
from app.schema.feature_request import \
get_feature_requests as db_get_feature_requests, \
get_feature_request_by_id as db_get_feature_request_by_id, \
create_feature_request as db_create_feature_request, \
update_feature_request as db_update_feature_request, \
delete_feature_request as db_delete_feature_request
from app.models.feature_request import FeatureRequestCreateRequest, FeatureRequestGetResponse, FeatureRequestCreateResponse

router = APIRouter()


@router.get('/', include_in_schema=False, status_code=200)
@router.get('', response_model=list, status_code=200, responses={400: {}})
async def get_feature_requests(
db: AgnosticDatabase = Depends(get_database_client),
):
logging.info('Received get feature requests request')

feature_requests = await db_get_feature_requests(db)

if None is feature_requests:
return Response(status_code=404)

return feature_requests


@router.get('/{id}', include_in_schema=False, status_code=200)
@router.get('/{id}', response_model=FeatureRequestGetResponse, status_code=200, responses={400: {}})
async def get_feature_request_by_id(
id: UUID,
db: AgnosticDatabase = Depends(get_database_client),
):
logging.info(
f'Received get feature request {uuid_masker(id)} request'
)

feature_request = await db_get_feature_request_by_id(
db,
id
)

if None is feature_request:
return Response(status_code=404)

return FeatureRequestGetResponse(
id=feature_request.get("_id"),
created_at=feature_request.get("created_at"),
updated_at=feature_request.get("updated_at"),
title=feature_request.get("title"),
content=feature_request.get("content"),
votes=feature_request.get("votes"),
category=feature_request.get("category"),
author_username=feature_request.get("author_username"),
comments=feature_request.get("comments"),
)


@router.post('/', include_in_schema=False, status_code=201)
@router.post('', response_model=FeatureRequestCreateResponse, status_code=201, responses={400: {}})
async def create_feature_request(
feature_request_data: FeatureRequestCreateRequest,
db: AgnosticDatabase = Depends(get_database_client)
):
logging.info('Received create feature request request')

# TODO: return actual document, not the user's input
feature_request_db = await db_create_feature_request(
db,
feature_request_data.title,
feature_request_data.content,
feature_request_data.author_username
)

return FeatureRequestCreateResponse(id=feature_request_db.id)


@router.put('/{id}', include_in_schema=False, status_code=200)
@router.put('/{id}', status_code=200, responses={400: {}})
async def update_feature_request(
id: UUID,
feature_request_data: FeatureRequestCreateRequest,
db: AgnosticDatabase = Depends(get_database_client),
):
logging.info(
f'Received update feature request {uuid_masker(id)} request'
)

feature_request_db = await db_update_feature_request(
db,
id,
feature_request_data.model_dump()
)
if None is feature_request_db:
raise UnprocessableError([])

return FeatureRequestCreateResponse(id=feature_request_db.get("_id"))


@router.delete('/{id}', include_in_schema=False, status_code=200)
@router.delete('/{id}', status_code=200, responses={400: {}})
async def delete_feature_request(
id: UUID,
db: AgnosticDatabase = Depends(get_database_client),
):
logging.info(
f'Received delete feature request {uuid_masker(id)} request'
)

feature_request_db = await db_delete_feature_request(
db,
id,
)
if None is feature_request_db:
raise UnprocessableError([])
return Response(status_code=204)
Empty file added server/app/common/__init__.py
Empty file.
38 changes: 38 additions & 0 deletions server/app/common/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from fastapi.responses import JSONResponse


class BaseErrResp(Exception):
def __init__(self, status: int, title: str, details: list) -> None:
self.__status = status
self.__title = title
self.__detail = details

def gen_err_resp(self) -> JSONResponse:
return JSONResponse(
status_code=self.__status,
content={
"type": "about:blank",
'title': self.__title,
'status': self.__status,
'detail': self.__detail
}
)


class BadRequest(BaseErrResp):
def __init__(self, details: list):
super(BadRequest, self).__init__(400, 'Bad Request', details)


class InternalError(BaseErrResp):
def __init__(self, details: list):
super(InternalError, self).__init__(500, 'Internal Error', details)


class UnprocessableError(BaseErrResp):
def __init__(self, details: list):
super(UnprocessableError, self).__init__(
422,
'Unprocessable Entity',
details
)
16 changes: 16 additions & 0 deletions server/app/common/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import re
from uuid import UUID


def to_lower_camel_case(string: str) -> str:
split_str = string.split('_')
return split_str[0] + ''.join(word.capitalize() for word in split_str[1:])


def uuid_masker(exposed_uuid: str | UUID) -> str:
uuid_str = str(exposed_uuid)
return re.sub(
r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-",
'********-****-****-****-',
uuid_str
)
Empty file added server/app/config/__init__.py
Empty file.
30 changes: 30 additions & 0 deletions server/app/config/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
from dotenv import load_dotenv
import logging

from app.common.error import InternalError

load_dotenv()


class Config:
version = "0.1.0"
title = "UserVote API"
env = os.getenv('ENV')

app_settings = {
'mongo_db_name': os.getenv('DB_NAME'),
'mongo_username': os.getenv('ATLAS_USERNAME'),
'mongo_password': os.getenv('ATLAS_PASSWORD'),
'max_db_conn_count': os.getenv('MAX_CONNECTIONS_COUNT'),
'min_db_conn_count': os.getenv('MIN_CONNECTIONS_COUNT'),
}

@classmethod
def validate_app_settings(cls):
for k, v in cls.app_settings.items():
if v is None:
logging.error(f'Config variable error. {k} cannot be None')
raise InternalError([{"message": "Server configure error"}])
else:
logging.info(f'Config variable {k} is {v}')
Loading

0 comments on commit 803e534

Please sign in to comment.