From 49950d3430dfcece6b5633be4dc1ca57f9199801 Mon Sep 17 00:00:00 2001 From: iamserda <12580399+iamserda@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:22:10 -0500 Subject: [PATCH 1/4] feat: enhance .gitignore, add database engine and models, and update schemas --- .gitignore | 2 ++ Makefile | 19 ++++++++++++++----- db/db.py | 26 ++++++++++++++++++++++++++ src/models/models.py | 32 ++++++++++++++++++++++++++++++++ src/models/schemas.py | 4 ---- 5 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 db/db.py create mode 100644 src/models/models.py diff --git a/.gitignore b/.gitignore index 4504ea0..597aaff 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,5 @@ example.db # cache **/*.ruff_cache +**/engineering_notes.md +db/shorties_datastore.db diff --git a/Makefile b/Makefile index 353ef11..cdb3d8e 100644 --- a/Makefile +++ b/Makefile @@ -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: @@ -29,10 +29,19 @@ 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 + @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" # Docker up: diff --git a/db/db.py b/db/db.py new file mode 100644 index 0000000..6c13165 --- /dev/null +++ b/db/db.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import os + +from sqlmodel import create_engine +from sqlmodel import Session + + +def app_db_engine(db_url: str = "sqlite:///memory", debug_mode: bool = False): + try: + if not isinstance(db_url, str): + raise ValueError("") + if db_url: + return create_engine(url=db_url, echo=debug_mode) + else: + raise ValueError("Could not create a ") + except ValueError as valErr: + print(f"error: {valErr}") # TODO: Log + + +if __name__ == "__main__": + DATABASE_URL = os.getenv("DATABASE_URL") + if DATABASE_URL: + db_ngin = app_db_engine(db_url=DATABASE_URL) + if db_ngin: + session = Session(db_ngin) diff --git a/src/models/models.py b/src/models/models.py new file mode 100644 index 0000000..5245896 --- /dev/null +++ b/src/models/models.py @@ -0,0 +1,32 @@ +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, default=None) + user_submitted_url: str | None = Field(default=None) + system_normalized_url: str | None = Field(default=None) + status: str + redirect_code: str | None = Field(default="302") + brand: str | None = None + tags: list[str] | None = [] + notes: str | None = None + # time stamps + created_by_user_id: str | None = None + created_on: str | None = None + last_updated_on: str | None = None + expires_on: str | None = None + total_click_count: int = 0 + recently_clicked_on: str | None = None + + +class ClickEvent(SQLModel): + event_id: int | None = Field(primary_key=True, default=None) + shortilink_id: int = Field(foreign_key=ShortiLink.id) + timestamp: str | None = None + visitor_id: str | None = None + ip_hash: str | None = None diff --git a/src/models/schemas.py b/src/models/schemas.py index 4176b1d..b1f2ec2 100644 --- a/src/models/schemas.py +++ b/src/models/schemas.py @@ -17,10 +17,6 @@ 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 UrlResponseModel(BaseModel): key: str | None = None From 778bf7da4fc00a8c714260b202b22716b375b1eb Mon Sep 17 00:00:00 2001 From: iamserda <12580399+iamserda@users.noreply.github.com> Date: Thu, 12 Feb 2026 02:21:43 -0500 Subject: [PATCH 2/4] feat: restructuring app for better devexp, layer with new models and schemas; remove legacy code; introducing sqlite db for now during development. will move to dockerized-postgres datastore instance later --@iamserda --- db/db.py | 26 --------------------- compose.yaml => docker-compose.yaml | 0 src/app/db/db.py | 27 ++++++++++++++++++++++ src/app/db/models/models.py | 11 +++++++++ src/app/main.py | 5 ++-- src/{models => app/schemas}/schemas.py | 0 src/models/__init__.py | 0 src/models/models.py | 32 -------------------------- 8 files changed, 40 insertions(+), 61 deletions(-) delete mode 100644 db/db.py rename compose.yaml => docker-compose.yaml (100%) create mode 100644 src/app/db/db.py create mode 100644 src/app/db/models/models.py rename src/{models => app/schemas}/schemas.py (100%) delete mode 100644 src/models/__init__.py delete mode 100644 src/models/models.py diff --git a/db/db.py b/db/db.py deleted file mode 100644 index 6c13165..0000000 --- a/db/db.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import os - -from sqlmodel import create_engine -from sqlmodel import Session - - -def app_db_engine(db_url: str = "sqlite:///memory", debug_mode: bool = False): - try: - if not isinstance(db_url, str): - raise ValueError("") - if db_url: - return create_engine(url=db_url, echo=debug_mode) - else: - raise ValueError("Could not create a ") - except ValueError as valErr: - print(f"error: {valErr}") # TODO: Log - - -if __name__ == "__main__": - DATABASE_URL = os.getenv("DATABASE_URL") - if DATABASE_URL: - db_ngin = app_db_engine(db_url=DATABASE_URL) - if db_ngin: - session = Session(db_ngin) diff --git a/compose.yaml b/docker-compose.yaml similarity index 100% rename from compose.yaml rename to docker-compose.yaml diff --git a/src/app/db/db.py b/src/app/db/db.py new file mode 100644 index 0000000..b3150e2 --- /dev/null +++ b/src/app/db/db.py @@ -0,0 +1,27 @@ +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.") + return create_engine(db_url, echo=dev_mode) + except ValueError as valErr: + print("url-path-error:", valErr) + return None + + +if __name__ == "__main__": + DATABASE_URL = os.getenv("DATABASE_URL") + DEV_ENV = os.getenv("DEV_ENV") + 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 diff --git a/src/app/db/models/models.py b/src/app/db/models/models.py new file mode 100644 index 0000000..2f888d1 --- /dev/null +++ b/src/app/db/models/models.py @@ -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) diff --git a/src/app/main.py b/src/app/main.py index 2e30848..c07c5c2 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,13 +1,12 @@ from __future__ import annotations from app.alnumgen import alnum_generator +from app.schemas.schemas import URLRequestModel +from app.schemas.schemas import UrlResponseModel 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 app = FastAPI(title="Shorties App") diff --git a/src/models/schemas.py b/src/app/schemas/schemas.py similarity index 100% rename from src/models/schemas.py rename to src/app/schemas/schemas.py diff --git a/src/models/__init__.py b/src/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/models/models.py b/src/models/models.py deleted file mode 100644 index 5245896..0000000 --- a/src/models/models.py +++ /dev/null @@ -1,32 +0,0 @@ -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, default=None) - user_submitted_url: str | None = Field(default=None) - system_normalized_url: str | None = Field(default=None) - status: str - redirect_code: str | None = Field(default="302") - brand: str | None = None - tags: list[str] | None = [] - notes: str | None = None - # time stamps - created_by_user_id: str | None = None - created_on: str | None = None - last_updated_on: str | None = None - expires_on: str | None = None - total_click_count: int = 0 - recently_clicked_on: str | None = None - - -class ClickEvent(SQLModel): - event_id: int | None = Field(primary_key=True, default=None) - shortilink_id: int = Field(foreign_key=ShortiLink.id) - timestamp: str | None = None - visitor_id: str | None = None - ip_hash: str | None = None From 26e55de12be2e127c46afa82366419f8d72c15f5 Mon Sep 17 00:00:00 2001 From: iamserda <12580399+iamserda@users.noreply.github.com> Date: Thu, 12 Feb 2026 03:25:22 -0500 Subject: [PATCH 3/4] feat: #1: introducing a file-based RDMS via SQLite. Current model is extremely basic to get things working quickly. Will add both analytics data-table, and more columns to the current shortilink table. #2: streamlined database engine creation and improved error handling; #3L removed unused code and simplified main app initialization --@iamserda --- src/app/db/db.py | 11 +- src/app/main.py | 254 ++++++++++++++++++++++------------------------- 2 files changed, 124 insertions(+), 141 deletions(-) diff --git a/src/app/db/db.py b/src/app/db/db.py index b3150e2..bc31760 100644 --- a/src/app/db/db.py +++ b/src/app/db/db.py @@ -10,15 +10,20 @@ def db_engine_factory(db_url: str, dev_mode=False): try: if db_url is None: raise ValueError("A URL path is required.") - return create_engine(db_url, echo=dev_mode) + 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) + print("url-path-error:", valErr) # TODO: Logger! return None if __name__ == "__main__": DATABASE_URL = os.getenv("DATABASE_URL") - DEV_ENV = os.getenv("DEV_ENV") + 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: diff --git a/src/app/main.py b/src/app/main.py index c07c5c2..69ad4ea 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,52 +1,29 @@ from __future__ import annotations -from app.alnumgen import alnum_generator -from app.schemas.schemas import URLRequestModel -from app.schemas.schemas import UrlResponseModel +import os + +from app.db.db import db_engine_factory +from app.db.models.models import ShortiLink from fastapi import APIRouter from fastapi import FastAPI -from fastapi import HTTPException -from pydantic import AnyUrl +from sqlmodel import select +from sqlmodel import Session +from sqlmodel import SQLModel + +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/") @@ -55,104 +32,105 @@ 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 - try: - if not 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.", - ) - - except HTTPException as err: - # todo: log error, send failure message, suggestions for retrying. - print(err) - raise err - except ValueError as err: - # todo: log error, send failure message, suggestions for retrying. - print(f"invalid-data-error: {err}") - return { - "key": f"{key}", - "url": None, - "status": "failure", - "message": f"value-not-found-error: {err}", - } - except TypeError as err: - # todo: log error, send failure message, suggestions for retrying. - print(f"invalid-data-error: {err}") - return { - "key": f"{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) +def display_all() -> list: 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", - ) - 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 - raise HTTPException( - status_code=404, - detail="status='failure', message='We could not create a new key at this moment!'", - ) - except HTTPException as err: - print(err) # todo: logg - raise err + if not db_engine: + raise ValueError("Error with DB Engine!") + with Session(db_engine) as session: + select_statement = select(ShortiLink) + results = session.exec(statement=select_statement).all() + for r in results: + print(r) + return list(results) + except Exception as err: + print("Error:", err) + return [] + + +# @api_router.get(f"{api_version}/redirect/") +# def get_url(key: str) -> dict: +# store = shorti_links +# try: +# if not 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.", +# ) + +# except HTTPException as err: +# # todo: log error, send failure message, suggestions for retrying. +# print(err) +# raise err +# except ValueError as err: +# # todo: log error, send failure message, suggestions for retrying. +# print(f"invalid-data-error: {err}") +# return { +# "key": f"{key}", +# "url": None, +# "status": "failure", +# "message": f"value-not-found-error: {err}", +# } +# except TypeError as err: +# # todo: log error, send failure message, suggestions for retrying. +# print(f"invalid-data-error: {err}") +# return { +# "key": f"{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: str = alnum_generator() +# print(url_item) +# try: +# while key in store: +# key = alnum_generator() + +# new_brand: str = url_item.brand +# new_url: str = url_item.url +# store[key]: dict = {"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", +# ) +# 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 +# raise HTTPException( +# status_code=404, +# detail="status='failure', message='We could not create a new key at this moment!'", +# ) +# except HTTPException as err: +# print(err) # todo: logg +# raise err app.include_router(api_router) From fdde526768c0d2eb6ac03688335d289443e569d2 Mon Sep 17 00:00:00 2001 From: iamserda <12580399+iamserda@users.noreply.github.com> Date: Thu, 12 Feb 2026 06:24:51 -0500 Subject: [PATCH 4/4] feat: can create new shorties, can read all shorties in the db, can request shorti based on its key, will implement just providing the short url and service redirect request using either 301 or 302 to the new resource permanent url. --@iamserda --- .gitignore | 2 +- Makefile | 7 -- src/app/main.py | 218 ++++++++++++++++++++++--------------- src/app/schemas/schemas.py | 8 +- 4 files changed, 136 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 597aaff..fe0b367 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,4 @@ example.db # cache **/*.ruff_cache **/engineering_notes.md -db/shorties_datastore.db +**/*.db diff --git a/Makefile b/Makefile index cdb3d8e..f10f03f 100644 --- a/Makefile +++ b/Makefile @@ -42,10 +42,3 @@ precommit-all: @echo "✅ mypy-typecheck checks passed" @echo "✅ pytest-test checks passed" @echo "✅ precommit(--stage-files-only) checks passed" - -# Docker -up: - docker compose up - -down: - docker compose down diff --git a/src/app/main.py b/src/app/main.py index 69ad4ea..114dc01 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,15 +1,24 @@ 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 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) @@ -32,105 +41,136 @@ def healthz() -> dict: @api_router.get(f"{api_version}/display/") -def display_all() -> list: +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) - results = session.exec(statement=select_statement).all() - for r in results: - print(r) - return list(results) + return session.exec(statement=select_statement).all() except Exception as err: print("Error:", err) return [] -# @api_router.get(f"{api_version}/redirect/") -# def get_url(key: str) -> dict: -# store = shorti_links -# try: -# if not 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.", -# ) - -# except HTTPException as err: -# # todo: log error, send failure message, suggestions for retrying. -# print(err) -# raise err -# except ValueError as err: -# # todo: log error, send failure message, suggestions for retrying. -# print(f"invalid-data-error: {err}") -# return { -# "key": f"{key}", -# "url": None, -# "status": "failure", -# "message": f"value-not-found-error: {err}", -# } -# except TypeError as err: -# # todo: log error, send failure message, suggestions for retrying. -# print(f"invalid-data-error: {err}") -# return { -# "key": f"{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: str = alnum_generator() -# print(url_item) -# try: -# while key in store: -# key = alnum_generator() - -# new_brand: str = url_item.brand -# new_url: str = url_item.url -# store[key]: dict = {"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", -# ) -# 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 -# raise HTTPException( -# status_code=404, -# detail="status='failure', message='We could not create a new key at this moment!'", -# ) -# except HTTPException as err: -# print(err) # todo: logg -# raise err +@api_router.get(f"{api_version}/redirect/", status_code=301) +def get_url(shorti_key: str | dict | GetURLRequestModel) -> GetUrlResponseModel | dict: + try: + print(shorti_key) + if not shorti_key: + raise ValueError("Invalid, user did not provide a key.") + + 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. + print(err) + raise err + except ValueError as err: + # todo: log error, send failure message, suggestions for retrying. + print(f"invalid-data-error: {err}") + return { + "key": f"{shorti_key}", + "url": None, + "status": "failure", + "message": f"value-not-found-error: {err}", + } + except TypeError as err: + # todo: log error, send failure message, suggestions for retrying. + print(f"invalid-data-error: {err}") + return { + "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: 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 url_item and len(url_item.url) <= 3: + raise HTTPException( + status_code=404, + detail="Invalid submission, missing url. We cannot create a new shorti without a valid url input.", + ) + + 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) diff --git a/src/app/schemas/schemas.py b/src/app/schemas/schemas.py index b1f2ec2..cc4a968 100644 --- a/src/app/schemas/schemas.py +++ b/src/app/schemas/schemas.py @@ -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 @@ -18,7 +18,11 @@ def __str__(self): return f"brand: {self.brand}, url: {self.url}" -class UrlResponseModel(BaseModel): +class GetURLRequestModel(BaseModel): + key: str + + +class GetUrlResponseModel(BaseModel): key: str | None = None brand: str | None = None url: AnyUrl | str