Skip to content

Commit

Permalink
Merge pull request #79 from ishefi/add-pre-commit-hooks
Browse files Browse the repository at this point in the history
Add pre commit hooks
  • Loading branch information
Iddoyadlin authored Dec 31, 2023
2 parents daa7ed0 + e03bfb4 commit 9234fe7
Show file tree
Hide file tree
Showing 33 changed files with 1,009 additions and 492 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Hooks

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
pre-commit-hooks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: snok/install-poetry@v1
with:
virtualenvs-create: false
- name: Install Dependancies
run: |
poetry install --no-root --no-interaction
- uses: pre-commit/[email protected]

18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: local
hooks:
- id: ruff
name: ruff
entry: ruff --fix
language: system
types: [python]
- id: ruff-format
name: ruff format
entry: ruff format
language: system
types: [python]
- id: mypy
name: MyPy
entry: mypy
language: system
types: [python]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Hebrew Semantle
A Hebrew version of [Semantle](https://semantle.com/).

[![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)


## Installation

Extract word2vec model in repository.
Expand Down
56 changes: 38 additions & 18 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
from __future__ import annotations

import hashlib
import os
import time
from collections import defaultdict
from datetime import datetime, timedelta
from datetime import datetime
from datetime import timedelta
from typing import TYPE_CHECKING

import uvicorn
from fastapi.staticfiles import StaticFiles
from common import config
from common.session import get_mongo, get_redis, get_model

from fastapi import FastAPI, Request, status
from fastapi import FastAPI
from fastapi import Request
from fastapi import Response
from fastapi import status
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles

from routers import routers
from common import config
from common.session import get_model
from common.session import get_mongo
from common.session import get_redis
from logic.user_logic import UserLogic
from routers import routers

if TYPE_CHECKING:
from typing import Awaitable
from typing import Callable

STATIC_FOLDER = "static"
js_hasher = hashlib.sha3_256()
Expand All @@ -38,9 +50,7 @@
app.state.notification = config.notification
app.state.js_version = JS_VERSION
app.state.css_version = CSS_VERSION
app.state.model = get_model(
mongo=app.state.mongo.word2vec2, has_model=hasattr(config, "model_zip_id")
)
app.state.model = get_model()
app.state.google_app = config.google_app


Expand All @@ -55,7 +65,7 @@
app.include_router(router)


def request_is_limited(key: str):
def request_is_limited(key: str) -> bool:
now = int(time.time())
current = now - now % app.state.period
if app.state.current_timeframe != current:
Expand All @@ -69,27 +79,37 @@ def request_is_limited(key: str):
int, {ip: usage for ip, usage in app.state.usage.items() if usage > 0}
)
app.state.usage[key] += 1
return app.state.usage[key] > app.state.limit
if app.state.usage[key] > app.state.limit:
return True
else:
return False


def get_idenitifier(request: Request):
def get_idenitifier(request: Request) -> str:
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host
if request.client:
return request.client.host
else:
return "unknown"


@app.middleware("http")
async def is_limited(request: Request, call_next):
async def is_limited(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
identifier = get_idenitifier(request)
if request_is_limited(key=identifier):
return JSONResponse(status_code=status.HTTP_429_TOO_MANY_REQUESTS)
return JSONResponse(content="", status_code=status.HTTP_429_TOO_MANY_REQUESTS)
response = await call_next(request)
return response


@app.middleware("http")
async def get_user(request: Request, call_next):
async def get_user(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
if session_id := request.cookies.get("session_id"):
mongo = request.app.state.mongo
session = await mongo.sessions.find_one({"session_id": session_id})
Expand All @@ -104,7 +124,7 @@ async def get_user(request: Request, call_next):


@app.get("/health")
async def health():
async def health() -> dict[str, str]:
return {"message": "Healthy!"}


Expand Down
21 changes: 16 additions & 5 deletions common/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
from __future__ import annotations

import os
import sys
from omegaconf import OmegaConf
from pathlib import Path
from typing import TYPE_CHECKING

from omegaconf import OmegaConf

if TYPE_CHECKING:
from typing import Any

thismodule = sys.modules[__name__]

conf = OmegaConf.create()
if (config_path := Path(__file__).parent.parent.resolve() / 'config.yaml').exists():
if (config_path := Path(__file__).parent.parent.resolve() / "config.yaml").exists():
conf.merge_with(OmegaConf.load(config_path))
if yaml_str := os.environ.get('YAML_CONFIG_STR'):
if yaml_str := os.environ.get("YAML_CONFIG_STR"):
conf.merge_with(OmegaConf.create(yaml_str))
for k, v in conf.items():
setattr(thismodule, k, v)


def __getattr__(name: str) -> Any:
if name in conf:
return conf[name]
raise AttributeError(f"module {__name__} has no attribute {name}")
4 changes: 2 additions & 2 deletions common/consts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime

VEC_SIZE = '100f'
FIRST_DATE = datetime(2022, 2, 21).date()
VEC_SIZE = "100f"
FIRST_DATE = datetime(2022, 2, 21).date()
2 changes: 1 addition & 1 deletion common/logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging


def setup_logger():
def setup_logger() -> logging.Logger:
log = logging.getLogger("web app")
log.setLevel(logging.DEBUG)
return log
Expand Down
6 changes: 4 additions & 2 deletions common/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
import datetime
from typing import Optional

from pydantic import BaseModel
from pydantic import validator

Expand All @@ -21,11 +22,12 @@ class UserStatistics(BaseModel):
total_games_won: int
average_guesses: float

@validator('average_guesses')
def result_check(cls, v):
@validator("average_guesses") # TODO: use some other parsing method
def result_check(cls, v: float) -> float:
...
return round(v, 2)


class Subscription(BaseModel):
verification_token: str
message_id: str
Expand Down
24 changes: 15 additions & 9 deletions common/session.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import gensim.models.keyedvectors as word2vec
from motor.motor_asyncio import AsyncIOMotorClient as MongoClient
from redis.asyncio import Redis
from common.logger import logger

from common import config
from model import GensimModel

if TYPE_CHECKING:
from typing import Any

import motor.core


def get_mongo():
def get_mongo() -> motor.core.AgnosticDatabase[Any]:
return MongoClient(config.mongo).Semantle


def get_redis():
def get_redis() -> Redis[Any]:
return Redis.from_url(config.redis, decode_responses=True, max_connections=10)


def get_model(mongo=None, has_model=False):
if has_model is not None:
logger.info("using model")
import gensim.models.keyedvectors as word2vec
return GensimModel(word2vec.KeyedVectors.load("model.mdl").wv)
raise Exception('couldnt find model')
def get_model() -> GensimModel:
return GensimModel(word2vec.KeyedVectors.load("model.mdl").wv)
3 changes: 3 additions & 0 deletions common/typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import numpy.typing

np_float_arr = numpy.typing.NDArray[numpy.float32]
6 changes: 4 additions & 2 deletions download_model.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import gdown
import shutil

import gdown

from common.config import conf

if __name__ == "__main__":
url = f'https://drive.google.com/uc?id={conf.model_zip_id}'
url = f"https://drive.google.com/uc?id={conf.model_zip_id}"
destination = "model.zip"
gdown.download(url, destination, quiet=False)
shutil.unpack_archive("model.zip")
33 changes: 20 additions & 13 deletions logic/auth_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,51 @@

import datetime
import uuid
from typing import TYPE_CHECKING

from google.oauth2 import id_token
from google.auth.transport import requests
from typing import TYPE_CHECKING
from google.oauth2 import id_token

from logic.user_logic import UserLogic

if TYPE_CHECKING:
import pymongo.database
from typing import Any

import motor.core


class AuthLogic:
def __init__(self, mongo: pymongo.database.Database, auth_client_id: str) -> None:
def __init__(
self, mongo: motor.core.AgnosticDatabase[Any], auth_client_id: str
) -> None:
self.user_logic = UserLogic(mongo)
self.sessions = mongo.sessions
self.auth_client_id = auth_client_id

async def session_id_from_credential(self, credential) -> str:
async def session_id_from_credential(self, credential: str) -> str:
user_info = self._verify_credential(credential)
email = user_info["email"]
user = await self.user_logic.get_user(email)
if user is None:
user = await self.user_logic.create_user(user_info)
session_id = uuid.uuid4().hex
await self.sessions.insert_one({
"session_id": session_id,
"user_email": user["email"],
"session_start": datetime.datetime.utcnow(),
})
await self.sessions.insert_one(
{
"session_id": session_id,
"user_email": user["email"],
"session_start": datetime.datetime.utcnow(),
}
)
return session_id

def _verify_credential(self, credential):
def _verify_credential(self, credential: str) -> dict[str, Any]:
try:
return id_token.verify_oauth2_token(
id_info: dict[str, Any] = id_token.verify_oauth2_token(
credential, requests.Request(), self.auth_client_id
)
return id_info
except ValueError:
raise ValueError("Invalid credential", 401324)

async def logout(self, session_id):
async def logout(self, session_id: str) -> None:
await self.sessions.delete_one({"session_id": session_id})
Loading

0 comments on commit 9234fe7

Please sign in to comment.