Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,5 @@ example.db

# cache
**/*.ruff_cache
**/engineering_notes.md
**/*.db
26 changes: 14 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
.PHONY: help install test lint fmt format typecheck precommit precommit-all

help:
@echo "Targets: install, test, lint, fmt, typecheck, ci, activate"
@echo "Targets: help install test lint fmt format typecheck precommit precommit-all"

activate:
@echo 'To activate virtual env, run the following in your shell:'
@echo 'eval "$$(make -s activate_venv)"'
@echo 'eval "$$(make -s env)"'

activate_venv:
venv:
poetry env activate

install:
Expand All @@ -29,14 +29,16 @@ typecheck:
poetry run pre-commit run -v mypy

precommit: lint format typecheck test
@echo "precommit checks passed"
@echo "✅ ruff-lint checks passed"
@echo "✅ ruff-format checks passed"
@echo "✅ mypy-typecheck checks passed"
@echo "✅ pytest-test checks passed"
@echo "✅ precommit(--stage-files-only) checks passed"

precommit-all: lint format typecheck test
precommit-all:
poetry run pre-commit run --all-files

# Docker
up:
docker compose up

down:
docker compose down
@echo "✅ ruff-lint checks passed"
@echo "✅ ruff-format checks passed"
@echo "✅ mypy-typecheck checks passed"
@echo "✅ pytest-test checks passed"
@echo "✅ precommit(--stage-files-only) checks passed"
File renamed without changes.
32 changes: 32 additions & 0 deletions src/app/db/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

import os

from sqlmodel import create_engine
from sqlmodel import SQLModel


def db_engine_factory(db_url: str, dev_mode=False):
try:
if db_url is None:
raise ValueError("A URL path is required.")
db_engine = create_engine(db_url, echo=dev_mode)
if db_engine is None:
raise ValueError(
"Failed to create DB ENGINE. Check the URL argument. Check sys/admin/dev logs for more details!"
)
return db_engine
except ValueError as valErr:
print("url-path-error:", valErr) # TODO: Logger!
return None


if __name__ == "__main__":
DATABASE_URL = os.getenv("DATABASE_URL")
DEV_ENV: bool = os.getenv("DEV_ENV", "False") == "True"
if DATABASE_URL:
db_engine = db_engine_factory(db_url=DATABASE_URL, dev_mode=DEV_ENV)
if db_engine:
SQLModel.metadata.create_all(
db_engine
) # will create a DB without any table
11 changes: 11 additions & 0 deletions src/app/db/models/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

from sqlmodel import Field
from sqlmodel import SQLModel


class ShortiLink(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
shorti_key: str = Field(index=True)
shorti_url: str = Field(index=True)
brand: str | None = Field(default=None)
231 changes: 124 additions & 107 deletions src/app/main.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,38 @@
from __future__ import annotations

import logging
import os
from collections.abc import Sequence

from app.alnumgen import alnum_generator
from app.db.db import db_engine_factory
from app.db.models.models import ShortiLink
from app.schemas.schemas import GetURLRequestModel
from app.schemas.schemas import GetUrlResponseModel
from app.schemas.schemas import NewUrlSubmissionModel
from fastapi import APIRouter
from fastapi import FastAPI
from fastapi import HTTPException
from models.schemas import URLRequestModel
from models.schemas import UrlResponseModel
from pydantic import AnyUrl
# from app.constants import KEY_MAX
from sqlmodel import select
from sqlmodel import Session
from sqlmodel import SQLModel

logger = logging.getLogger(__name__)

DATABASE_URL = "sqlite:///src/app/db/SHORTIES_DATABASE.db"
DEV_ENV: bool = os.getenv("DEV_ENV", "False") == "True"
db_engine = db_engine_factory(db_url=DATABASE_URL, dev_mode=DEV_ENV)

if db_engine:
SQLModel.metadata.create_all(db_engine)

app = FastAPI(title="Shorties App")
api_router = APIRouter()
api_version = "/v1"
shorti_links: dict = {
"scap": {"brand": "scap", "url": "https://www.scapital.com"},
"goog": {"brand": "goog", "url": "https://www.google.com"},
"meta": {"brand": "meta", "url": "https://www.facebook.com"},
"twit": {"brand": "twit", "url": "https://www.twitter.com"},
"link": {"brand": "link", "url": "https://www.linkedin.com"},
"gith": {"brand": "gith", "url": "https://www.github.com"},
"redd": {"brand": "redd", "url": "https://www.reddit.com"},
"yout": {"brand": "yout", "url": "https://www.youtube.com"},
"inst": {"brand": "inst", "url": "https://www.instagram.com"},
"micr": {"brand": "micr", "url": "https://www.microsoft.com"},
"appl": {"brand": "appl", "url": "https://www.apple.com"},
"amaz": {"brand": "amaz", "url": "https://www.amazon.com"},
"netf": {"brand": "netf", "url": "https://www.netflix.com"},
"spof": {"brand": "spof", "url": "https://www.spotify.com"},
"slac": {"brand": "slac", "url": "https://www.slack.com"},
"drop": {"brand": "drop", "url": "https://www.dropbox.com"},
"adob": {"brand": "adob", "url": "https://www.adobe.com"},
"uber": {"brand": "uber", "url": "https://www.uber.com"},
"airb": {"brand": "airb", "url": "https://www.airbnb.com"},
"tesl": {"brand": "tesl", "url": "https://www.tesla.com"},
"nyti": {"brand": "nyti", "url": "https://www.nytimes.com"},
"wash": {"brand": "wash", "url": "https://www.washingtonpost.com"},
"bbc": {"brand": "bbc", "url": "https://www.bbc.com"},
"cnn": {"brand": "cnn", "url": "https://www.cnn.com"},
"espn": {"brand": "espn", "url": "https://www.espn.com"},
"pint": {"brand": "pint", "url": "https://www.pinterest.com"},
"tumblr": {"brand": "tumblr", "url": "https://www.tumblr.com"},
"quor": {"brand": "quor", "url": "https://www.quora.com"},
"yaho": {"brand": "yaho", "url": "https://www.yahoo.com"},
"ebay": {"brand": "ebay", "url": "https://www.ebay.com"},
"payp": {"brand": "payp", "url": "https://www.paypal.com"},
"tikt": {"brand": "tikt", "url": "https://www.tiktok.com"},
}
click_event: dict = {}
api_version = os.getenv("API_VERSION")


if not api_version:
api_version = "/v1"


@api_router.get(f"{api_version}/healthz/")
Expand All @@ -56,42 +41,56 @@ def healthz() -> dict:


@api_router.get(f"{api_version}/display/")
def display_all(max_results: int | None = None) -> dict:
store = shorti_links
if max_results is None or max_results <= 0 or max_results >= len(store):
return store
else:
results = {}
for i, (k, v) in enumerate(store.items()):
if i >= max_results:
break
else:
results[k] = v
return results


@api_router.get(f"{api_version}/redirect/")
def get_url(key: str) -> dict:
store = shorti_links
def display_all() -> Sequence:
try:
if not db_engine:
raise ValueError("Error with DB Engine!")
with Session(db_engine) as session:
select_statement = select(ShortiLink)
return session.exec(statement=select_statement).all()
except Exception as err:
print("Error:", err)
return []


@api_router.get(f"{api_version}/redirect/", status_code=301)
def get_url(shorti_key: str | dict | GetURLRequestModel) -> GetUrlResponseModel | dict:
try:
if not key:
print(shorti_key)
if not shorti_key:
raise ValueError("Invalid, user did not provide a key.")
if not isinstance(key, str):
raise TypeError(
"The key provided is not of a valid type. Key must be a str.",
)
if key in store:
return {
"key": f"{key}",
"url": store.get(key),
"status": "success",
"message": f'success: url matching "{key}" was found!',
}
else:
raise HTTPException(
status_code=404,
detail="failure: this key does not match our records. Verify the key and try again.",

if (
not isinstance(shorti_key, str)
and not isinstance(shorti_key, GetURLRequestModel)
and not isinstance(shorti_key, dict)
):
raise TypeError("The key provided is not of a valid type.")

with Session(db_engine) as current_session:
if isinstance(shorti_key, GetURLRequestModel):
shorti_key = shorti_key.key
elif isinstance(shorti_key, dict):
shorti_key = shorti_key["shorti_key"]

select_statement = select(ShortiLink).where(
ShortiLink.shorti_key == shorti_key
)
result = current_session.exec(statement=select_statement).all()
if result:
new_shorti = result[0]
return GetUrlResponseModel(
key=new_shorti.shorti_key,
brand=new_shorti.brand,
url=new_shorti.shorti_url,
message=f'success: url matching "{shorti_key}" was found!',
status="Success!",
)
else:
raise HTTPException(
status_code=404,
detail=f"message: Request-Failed: 'This key {shorti_key} does not match our records. Verify the key and try again.",
)

except HTTPException as err:
# todo: log error, send failure message, suggestions for retrying.
Expand All @@ -101,7 +100,7 @@ def get_url(key: str) -> dict:
# todo: log error, send failure message, suggestions for retrying.
print(f"invalid-data-error: {err}")
return {
"key": f"{key}",
"key": f"{shorti_key}",
"url": None,
"status": "failure",
"message": f"value-not-found-error: {err}",
Expand All @@ -110,50 +109,68 @@ def get_url(key: str) -> dict:
# todo: log error, send failure message, suggestions for retrying.
print(f"invalid-data-error: {err}")
return {
"key": f"{key}",
"key": f"{shorti_key}",
"url": None,
"status": "failure",
"message": f"type-validity-error: {err}",
}


@api_router.post(f"{api_version}/create/")
def create_url(url_item: URLRequestModel) -> UrlResponseModel:
store = shorti_links
key = alnum_generator()
print(url_item)
try:
while key in store:
key = alnum_generator()

new_brand = url_item.brand
new_url = url_item.url
store[key] = {"brand": new_brand, "url": new_url}
new_url_item = UrlResponseModel(
key=key,
brand=store[key]["brand"],
url=AnyUrl(store[key]["url"]),
status="success",
message="A key was successfully generated",
def create_url(url_item: NewUrlSubmissionModel) -> Sequence[GetUrlResponseModel]:
if url_item is None or not len(url_item.url):
raise HTTPException(
status_code=404,
detail="Invalid submission, either url or both url and brand are missing!",
)
if key in store:
return new_url_item

# failed = UrlResponseModel(
# key=key,
# brand="",
# url="",
# status="failure",
# message="We could not create a new key at this moment. Please try again later!",
# )
# return failed

if url_item and len(url_item.url) <= 3:
raise HTTPException(
status_code=404,
detail="status='failure', message='We could not create a new key at this moment!'",
detail="Invalid submission, missing url. We cannot create a new shorti without a valid url input.",
)
except HTTPException as err:
print(err) # todo: logg
raise err

key: str = alnum_generator()
new_shorties: list[GetUrlResponseModel] = []
try:
with Session(db_engine) as session:
select_statement = select(ShortiLink).where(ShortiLink.shorti_key == key)
result = session.exec(statement=select_statement).all()
while result:
key = alnum_generator()
select_statement = select(ShortiLink).where(
ShortiLink.shorti_key == key
)
result = session.exec(statement=select_statement).all()

new_shorti = ShortiLink(
shorti_key=key, shorti_url=url_item.url, brand=url_item.brand
)
session.add(new_shorti)
session.commit()

select_statement = select(ShortiLink).where(ShortiLink.shorti_key == key)
shorties: Sequence = session.exec(statement=select_statement).all()
if shorties:
new_shorties = [
GetUrlResponseModel(
key=shorti.shorti_key,
brand=shorti.brand,
url=shorti.shorti_url,
status="success",
message="A key was successfully generated. We stored your url! You can start using your new short url immediately.",
)
for shorti in shorties
]
else:
raise HTTPException(
status_code=404,
detail="status='failure', message='We could not create a new key at this moment!'",
)
return new_shorties
except HTTPException as httpErr:
logger.exception("HTTPException while creating shorti: %s", httpErr)
return new_shorties


app.include_router(api_router)
Expand Down
10 changes: 5 additions & 5 deletions src/models/schemas.py → src/app/schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pydantic import BaseModel


class URLRequestModel(BaseModel):
class NewUrlSubmissionModel(BaseModel):
brand: str | None = None
url: AnyUrl | str
expires_on: str | None = None
Expand All @@ -17,12 +17,12 @@ class URLRequestModel(BaseModel):
def __str__(self):
return f"brand: {self.brand}, url: {self.url}"

# @classmethod
# def __dict__(cls):
# return {"brand": cls.brand, "url": cls.url}

class GetURLRequestModel(BaseModel):
key: str

class UrlResponseModel(BaseModel):

class GetUrlResponseModel(BaseModel):
key: str | None = None
brand: str | None = None
url: AnyUrl | str
Expand Down
Empty file removed src/models/__init__.py
Empty file.