Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
melmi committed Mar 13, 2022
0 parents commit 310ef83
Show file tree
Hide file tree
Showing 38 changed files with 669 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 120
exclude = .env/
167 changes: 167 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@

# Created by https://www.toptal.com/developers/gitignore/api/django
# Edit at https://www.toptal.com/developers/gitignore?templates=django

### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media

# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/

### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo

# Django stuff:

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# End of https://www.toptal.com/developers/gitignore/api/django
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"python.linting.flake8Enabled": true,
"python.linting.enabled": true
}
Binary file added doc/diagram.drawio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions doc/diagram.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2022-03-13T03:46:27.746Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36" etag="FOYSFn7iCrOLh9TuYcWU" version="17.1.2" type="device"><diagram name="Page-1" id="c4acf3e9-155e-7222-9cf6-157b1a14988f">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</diagram></mxfile>
1 change: 1 addition & 0 deletions doc/readme
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
open the diagram using draw.io
22 changes: 22 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'urlshortner.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()
12 changes: 12 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
asgiref==3.5.0
decorator==5.1.1
Django==4.0.3
flake8==4.0.1
mccabe==0.6.1
punq==0.6.2
pycodestyle==2.8.0
pyflakes==2.4.0
qrcode==7.3.1
six==1.16.0
sqlparse==0.4.2
validators==0.18.2
Empty file added shortner/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions shortner/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions shortner/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class ShortnerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'shortner'
Empty file added shortner/domain/__inti__.py
Empty file.
6 changes: 6 additions & 0 deletions shortner/domain/boundaries/input/token_broker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from typing import Protocol


class TokenBroker(Protocol):
def get_token(self, url: str) -> str: ...
def get_url(self, token: str) -> str: ...
5 changes: 5 additions & 0 deletions shortner/domain/boundaries/output/random_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Protocol


class RandomGenerator(Protocol):
def get_next_int(self, stop: int) -> int: ...
5 changes: 5 additions & 0 deletions shortner/domain/boundaries/output/token_generator_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Protocol


class TokenGeneratorConfig(Protocol):
def get_token_length(self) -> int: ...
5 changes: 5 additions & 0 deletions shortner/domain/boundaries/output/url_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Protocol


class UrlValidator(Protocol):
def is_url_valid(self, url: str) -> bool: ...
8 changes: 8 additions & 0 deletions shortner/domain/boundaries/output/urltoken_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from typing import Protocol

from shortner.domain.entities.url_token import UrlToken


class UrlTokenRepository(Protocol):
def get_by_token(self, token: str) -> UrlToken: ...
def insert(self, data: UrlToken): ...
6 changes: 6 additions & 0 deletions shortner/domain/entities/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class NotFoundException(Exception):
pass


class BusinessException(Exception):
pass
7 changes: 7 additions & 0 deletions shortner/domain/entities/url_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from dataclasses import dataclass


@dataclass
class UrlToken:
url: str
token: str
33 changes: 33 additions & 0 deletions shortner/domain/services/token_broker_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from shortner.domain.boundaries.input.token_broker import TokenBroker
from shortner.domain.boundaries.output.url_validator import UrlValidator
from shortner.domain.boundaries.output.urltoken_repository import UrlTokenRepository
from shortner.domain.services.token_generator import TokenGenerator
from shortner.domain.entities.exceptions import BusinessException, NotFoundException
from shortner.domain.entities.url_token import UrlToken


class TokenBrokerService(TokenBroker):
def __init__(self,
repository: UrlTokenRepository,
url_validator: UrlValidator,
token_generator: TokenGenerator):
self.repository = repository
self.url_validator = url_validator
self.token_generator = token_generator

def get_token(self, url: str) -> str:
if not self.url_validator.is_url_valid(url):
raise BusinessException('url is not valid')

token = self.token_generator.get_token()

self.repository.insert(UrlToken(url, token))

return token

def get_url(self, token: str) -> str:
url_token = self.repository.get_by_token(token)
if url_token is None:
raise NotFoundException('token not found')

return url_token.url
19 changes: 19 additions & 0 deletions shortner/domain/services/token_generation_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from shortner.domain.services.token_generator import TokenGenerator
from shortner.domain.boundaries.output.random_generator import RandomGenerator
from shortner.domain.boundaries.output.token_generator_config import TokenGeneratorConfig


class TokenGenerationService(TokenGenerator):
def __init__(self,
token_generation_config: TokenGeneratorConfig,
random_generator: RandomGenerator):
self.token_generation_config = token_generation_config
self.random_generator = random_generator

CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'

def get_token(self) -> str:
chars = [self.CHARS[self.random_generator.get_next_int(len(self.CHARS))]
for i in range(self.token_generation_config.get_token_length())]
result = ''.join(chars)
return result
5 changes: 5 additions & 0 deletions shortner/domain/services/token_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from typing import Protocol


class TokenGenerator(Protocol):
def get_token(self) -> str: ...
40 changes: 40 additions & 0 deletions shortner/ioc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import punq

from shortner.domain.boundaries.input.token_broker import TokenBroker
from shortner.domain.boundaries.output.random_generator import RandomGenerator
from shortner.domain.boundaries.output.token_generator_config import TokenGeneratorConfig
from shortner.domain.boundaries.output.url_validator import UrlValidator
from shortner.domain.boundaries.output.urltoken_repository import UrlTokenRepository
from shortner.domain.services.token_broker_service import TokenBrokerService
from shortner.domain.services.token_generation_service import TokenGenerationService
from shortner.domain.services.token_generator import TokenGenerator
from shortner.services.default_random_generator_service import DefaultRandomGeneratorService
from shortner.services.env_token_generator_config import EnvTokenGeneratorConfig
from shortner.services.validators_url_validator_service import ValidatorsUrlValidatorService
from shortner.services.model_url_token_repository import ModelUrlTokenRepository

container = punq.Container()

container.register(TokenBroker, TokenBrokerService)
container.register(RandomGenerator, DefaultRandomGeneratorService)
container.register(UrlValidator, ValidatorsUrlValidatorService)
container.register(UrlTokenRepository, ModelUrlTokenRepository)
container.register(TokenGenerator, TokenGenerationService)
container.register(TokenGeneratorConfig, EnvTokenGeneratorConfig)


# based on https://github.com/django/django/blob/main/django/views/generic/base.py
def get_ioc_view(cls, **initkwargs):
def view(request, *args, **kwargs):
self = container.instantiate(cls, **initkwargs)
self.setup(request, *args, **kwargs)
return self.dispatch(request, *args, **kwargs)

view.view_class = cls
view.view_initkwargs = initkwargs
view.__doc__ = cls.__doc__
view.__module__ = cls.__module__
view.__annotations__ = cls.dispatch.__annotations__
view.__dict__.update(cls.dispatch.__dict__)

return view
Loading

0 comments on commit 310ef83

Please sign in to comment.