diff --git a/.gitignore b/.gitignore index 2e5f17d..c9fd033 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .pyenv/ .vscode/ __pycache__/ +slinky.db diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..710c173 --- /dev/null +++ b/config.yaml @@ -0,0 +1,2 @@ +--- +db: sqlite:///slinky.db diff --git a/requirements.txt b/requirements.txt index 84548f4..5a5c7fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ flask flask_bootstrap flask_wtf +pyyaml waitress Flask-SQLAlchemy diff --git a/slinky/__init__.py b/slinky/__init__.py index 0085bd3..cd019c8 100644 --- a/slinky/__init__.py +++ b/slinky/__init__.py @@ -4,12 +4,28 @@ Main code import random import string +from dataclasses import dataclass from datetime import datetime from typing import Optional +import sqlalchemy + from slinky import db +@dataclass +class Shortcode: + """ + Simple dataclass to allow for typing of Shortcodes + """ + + id: int # pylint: disable=invalid-name + shortcode: str + url: str + fixed_views: int + expiry: str + + def random_string(length: int = 4) -> str: """ Create a random, alphanumeric string of the length specified @@ -25,34 +41,80 @@ def random_string(length: int = 4) -> str: return ''.join(random.SystemRandom().choice(allowed_chars) for _ in range(length)) -def add_shortcode( - url: str, - length: int = 4, - fixed_views: int = 0, - expiry: datetime = datetime.max, -) -> str: +class Slinky: """ - Add a shortcode to the DB - - Args: - url (str): URL to redirect to - fixed_views (int, optional): number of views to serve before expiring. - Defaults to 0 (no limit). - expiry (int, optional): date of expiry. Defaults to 0 (no limit). - - Returns: - str: shortcode for the redirect + Class for Slinky """ - session = db.session() - shortcode = random_string(length=length) - dbentry = db.ShortURL( - shortcode=shortcode, - url=url, - fixed_views=fixed_views, - expiry=expiry, - ) - session.add(dbentry) - session.commit() - session.close() - return shortcode + def __init__(self, url: str) -> None: + self.db = db.ShortcodeDB(url) # pylint: disable=invalid-name + self.session = self.db.session() + + def add( + self, + url: str, + length: int = 4, + fixed_views: int = -1, + expiry: datetime = datetime.max, + ) -> str: + """ + Add a shortcode to the DB + + Args: + url (str): URL to redirect to + fixed_views (int, optional): number of views to serve before expiring. + Defaults to 0 (no limit). + expiry (int, optional): date of expiry. Defaults to 0 (no limit). + + Returns: + str: shortcode for the redirect + """ + shortcode = random_string(length=length) + + if self.get(shortcode).url: + raise ValueError(f'Shortcode {shortcode} already exists') + + dbentry = db.ShortURL( + shortcode=shortcode, + url=url, + fixed_views=fixed_views, + expiry=expiry, + ) + self.session.add(dbentry) + self.session.commit() + + return shortcode + + def get(self, shortcode: str) -> Shortcode: + """ + Return a Shortcode object for a given shortcode + + Args: + shortcode (str): the shortcode to look up + + Returns: + Shortcode: full Shortcode object for the given shortcode + """ + entry = self.session.query(db.ShortURL).filter_by(shortcode=shortcode).first() + + if entry: + return Shortcode( + entry.id, + entry.shortcode, + entry.url, + entry.fixed_views, + entry.expiry, + ) + return Shortcode(0, '', '', 0, '1970-01-01 00:00:00.000000') + + def remove_view(self, sc_id: int) -> None: + """ + Reduce the fixed views count by one + + Args: + id (int): ID of the DB entry to reduce the fixed_views count + """ + self.session.query(db.ShortURL).filter_by(id=sc_id).update( + {db.ShortURL.fixed_views: db.ShortURL.fixed_views - 1} + ) + self.session.commit() diff --git a/slinky/db.py b/slinky/db.py index 3a4c1de..cf68915 100644 --- a/slinky/db.py +++ b/slinky/db.py @@ -1,6 +1,8 @@ """ DB component """ +from dataclasses import dataclass + from sqlalchemy import Column, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Session, sessionmaker @@ -8,7 +10,7 @@ from sqlalchemy.orm import Session, sessionmaker Base = declarative_base() -class ShortURL(Base): # pylint: disable=too-few-public-methods +class ShortURL(Base): # type: ignore[misc, valid-type] # pylint: disable=too-few-public-methods """ Class to describe the DB schema for ShortURLs """ @@ -22,14 +24,21 @@ class ShortURL(Base): # pylint: disable=too-few-public-methods expiry = Column(Integer, unique=False, nullable=False) -def session() -> Session: +@dataclass +class ShortcodeDB: """ - Create a DB session + Class to represent the database + """ + url: str - Returns: - Session: the DB session object - """ - engine = create_engine('sqlite:////tmp/test.db') - Base.metadata.create_all(engine) - new_session = sessionmaker(bind=engine) - return new_session() + def session(self) -> Session: + """ + Create a DB session + + Returns: + Session: the DB session object + """ + engine = create_engine(self.url) + Base.metadata.create_all(engine) + new_session = sessionmaker(bind=engine) + return new_session() diff --git a/slinky/templates/add.html b/slinky/templates/add.html index 7f30f7a..9a943da 100644 --- a/slinky/templates/add.html +++ b/slinky/templates/add.html @@ -6,7 +6,7 @@