Skip to content

Commit 4eca674

Browse files
committed
release 0.2.0
1 parent 037c032 commit 4eca674

15 files changed

+445
-32
lines changed

.gitignore

+1-15
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,4 @@ __pycache__/
66
*.pyc
77
build/
88
.env
9-
/autogenerated-structure-template/app/core/__init__.py
10-
/autogenerated-structure-template/app/core/config.py
11-
/autogenerated-structure-template/app/__init__.py
12-
/autogenerated-structure-template/app/database.py
13-
/autogenerated-structure-template/app/main.py
14-
/autogenerated-structure-template/tests/__init__.py
15-
/autogenerated-structure-template/.gitignore
16-
/autogenerated-structure-template/.pre-commit-config.yaml
17-
/autogenerated-structure-template/docker-compose.yaml
18-
/autogenerated-structure-template/Dockerfile
19-
/autogenerated-structure-template/gino-fastapi-layout.svg
20-
/autogenerated-structure-template/LICENSE
21-
/autogenerated-structure-template/README.md
22-
/autogenerated-structure-template/requirements.txt
23-
/autogenerated-structure-template/setup.cfg
9+
autogenerated-structure-template/

README.md

+55-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## How to start
44

5-
Use python 3.9 or 3.10.
5+
Use python 3.10 or 3.11.
66

77
Create virtual environment
88
```bash
@@ -17,11 +17,11 @@ pip install -r requirements.txt
1717
The module `uvloop==0.14.0` is not being installed with python 3.11 due to an error `Could not build wheels for uvloop`.
1818
But it works with 3.10.
1919

20-
Create `.env` file in the root of project:
20+
Create `.env` file in the root of project with `PROJECT_NAME='Your-name-for-this-project'`
2121
```bash
22-
PROJECT_NAME='Your-name-for-this-project'
22+
touch .env
23+
echo "PROJECT_NAME='URLer'" > .env
2324
```
24-
e.g. set a name `PROJECT_NAME='URLer'`.
2525

2626
**Run server**
2727
```bash
@@ -30,12 +30,21 @@ e.g. set a name `PROJECT_NAME='URLer'`.
3030
_Note: 0.0.0.0 may not work in Safari browser_
3131

3232
## How to test
33+
34+
### In browser
3335
Open any link in any browser
3436

3537
* Swagger http://127.0.0.1:8080/docs
3638
* ReDoc http://127.0.0.1:8080/redoc
3739
* OpenAPI documentation (json) http://127.0.0.1:8080/openapi.json
3840

41+
### With pytest and TestClient
42+
Run all tests by
43+
```bash
44+
pytest
45+
```
46+
or create your own tests in `tests` folder. You may use tests in `test_routes.py` as template for your tests.
47+
3948
## A tidy up and a health check
4049

4150
Install additional "developer's" requirements
@@ -68,6 +77,48 @@ A static type checker tool
6877
mypy src
6978
```
7079

80+
# Тому кто будет это читать
81+
82+
## Способности проекта
83+
84+
Проект может:
85+
- генерировать случайные короткие ссылки (точнее, айдишки - из четырех 16-ричных цифр) -
86+
```
87+
POST `/shorten` json={'url': <ссылка>}
88+
```
89+
- выдавать полную ссылку по короткой ссылке
90+
```
91+
GET `/link?url_id=abcd`
92+
```
93+
- подсчитывать число переходов по ссылке (`RecordModel->used`)
94+
- удалять (deprecate) ссылки. Операция безвозвратная
95+
```
96+
PATCH `/deprecate?url_id=abcd`
97+
```
98+
- выдавать запись из базы данных о ссылке
99+
```
100+
GET `/info?url_id=abcd`
101+
```
102+
- выдавать ошибки. Например, при попытке передать неверную ссылку (например, "https://example .com")
103+
104+
105+
## Результаты
106+
107+
В ходе работы над этим заданием автору удалось **впервые**:
108+
- [x] Полноценно поработать с FastApi и подобными фреймворками в принципе
109+
- [x] Опробовать всю магию pydantic - действительно удобный инструмент как о нём ходили слухи
110+
- [x] Писать Post- и Patch- запросы
111+
- [x] Поработать с сопутствующими технологиями как uvicorn, FastApi TestClient и др.
112+
113+
Хотелось, но не удалось впервые применить (из-за нехватки времени на эти новые технологии):
114+
- [ ] работу с базой данных через ORM (в частности, SqlAlchemy) - вместо написаний каноничных SQL-запросов
115+
- [ ] внедрить базу данных в свой проект
116+
- [ ] понять чем схемы отличаются от моделей
117+
118+
На данный момент в качестве базы данных используется обычный словарь (класс `BD`),
119+
в котором ключу-айдишнику сопоставляется запись "базы данных".
120+
121+
71122
# Проектное задание четвёртого спринта
72123

73124
Спроектируйте и реализуйте сервис для создания сокращённой формы передаваемых URL и анализа активности их использования.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.pytest.ini_options]
22
addopts = '-sv'
3-
pythonpath = '.'
3+
pythonpath = 'src/'
44

55
[tool.ruff.lint]
66
extend-select=['I']

requirements-dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ flake8
33
mypy
44
ruff
55
watchgod==0.8.2
6+

requirements.txt

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
2-
fastapi==0.93.0
3-
pydantic==1.9.0
4-
python-dotenv==1.0.1
5-
uvicorn==0.18.2
6-
uvloop==0.17.0; sys_platform != "win32" and implementation_name == "cpython"
1+
fastapi~=0.109.2
2+
pydantic~=1.10
3+
python-dotenv~=1.0.1
4+
uvicorn~=0.27.0
5+
uvloop~=0.19.0; sys_platform != "win32" and implementation_name == "cpython"
6+
httpx~=0.26.0
7+
pytest~=7.0

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ exclude =
1010
per-file-ignores =
1111
*/settings.py:E501
1212
max-complexity = 10
13+
max-line-length = 120
File renamed without changes.

src/apiv1/base.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# for python 3.9
2+
from __future__ import annotations
3+
4+
import json
5+
import logging
6+
7+
from fastapi import APIRouter, HTTPException
8+
from pydantic import ValidationError
9+
10+
from src.models.base import RecordModel, UrlModel
11+
from src.services.base import CRUD
12+
13+
logger = logging.getLogger(__name__)
14+
logger.setLevel('DEBUG')
15+
16+
17+
router = APIRouter()
18+
19+
20+
@router.post('/shorten/', status_code=201)
21+
async def shorten_link(link: UrlModel):
22+
"""save link and return id"""
23+
logger.debug('link: %s', link.url)
24+
try:
25+
UrlModel(url=link.url)
26+
except ValidationError:
27+
raise HTTPException(status_code=422, detail='Input data is not a link')
28+
record: RecordModel = CRUD.create_record(link=link)
29+
30+
return record.json()
31+
32+
33+
@router.get('/link', status_code=307)
34+
async def return_link(url_id: str):
35+
"""return full link by id"""
36+
logger.debug('id: %s', url_id)
37+
record: RecordModel = CRUD.read_record(url_id)
38+
logger.debug(record)
39+
url = record.url_full
40+
return {'url_full': url}
41+
42+
43+
@router.get('/info')
44+
async def info(url_id: str):
45+
"""info about one link"""
46+
logger.debug('id: %s', url_id)
47+
# /info does not increase the usage of link
48+
record: RecordModel = CRUD.read_record(url_id, incr=False)
49+
return json.loads(record.json())
50+
51+
52+
@router.patch('/deprecate', status_code=200)
53+
async def deprecate(url_id: str):
54+
"""to deprecate (or "delete") a link"""
55+
CRUD.deprecate_record(url_id)
56+
return {}
57+
58+
59+
@router.post('/shorten-batch')
60+
async def shorten_links():
61+
"""batch upload - save many link and return their ids"""
62+
...
63+
return {'response': 'Not implemented'}
64+
65+
66+
@router.get('/ping')
67+
async def ping():
68+
return {'Is database available': False}
69+
70+
71+
@router.get('/hello') # hello world
72+
async def hello_world():
73+
return {'hello': 'world'}
74+
75+
76+
@router.get('/{action}')
77+
async def handler_other(action):
78+
return {'next page is asked but it does not exist': action}

src/core/config.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
"""
2-
Run through console:
3-
4-
uvicorn src.main:app --host 127.0.0.1 --port 8080
5-
additional notes are in the `../README.md` file
6-
"""
1+
from logging import config as logging_config
72
from pathlib import Path
83

94
from pydantic import BaseSettings
105

6+
from .logger import LOGGING
7+
8+
# logging settings
9+
logging_config.dictConfig(LOGGING)
10+
1111
BASE_DIR = Path.cwd().parent.parent
1212
# print('BASE_DIR', BASE_DIR)
1313

src/core/logger.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
This logging configuration was copied from one source without changes
3+
4+
It sets logging of uvicorn-server
5+
More information:
6+
https://docs.python.org/3/howto/logging.html
7+
https://docs.python.org/3/howto/logging-cookbook.html
8+
"""
9+
10+
LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
11+
LOG_DEFAULT_HANDLERS = [
12+
'console',
13+
]
14+
15+
16+
LOGGING = {
17+
'version': 1,
18+
'disable_existing_loggers': False,
19+
'formatters': {
20+
'verbose': {'format': LOG_FORMAT},
21+
'default': {
22+
'()': 'uvicorn.logging.DefaultFormatter',
23+
'fmt': '%(levelprefix)s %(message)s',
24+
'use_colors': None,
25+
},
26+
'access': {
27+
'()': 'uvicorn.logging.AccessFormatter',
28+
'fmt': "%(levelprefix)s %(client_addr)s - '%(request_line)s' %(status_code)s",
29+
},
30+
},
31+
'handlers': {
32+
'console': {
33+
'level': 'DEBUG',
34+
'class': 'logging.StreamHandler',
35+
'formatter': 'verbose',
36+
},
37+
'default': {
38+
'formatter': 'default',
39+
'class': 'logging.StreamHandler',
40+
'stream': 'ext://sys.stdout',
41+
},
42+
'access': {
43+
'formatter': 'access',
44+
'class': 'logging.StreamHandler',
45+
'stream': 'ext://sys.stdout',
46+
},
47+
},
48+
'loggers': {
49+
'': {
50+
'handlers': LOG_DEFAULT_HANDLERS,
51+
'level': 'INFO',
52+
},
53+
'uvicorn.error': {
54+
'level': 'INFO',
55+
},
56+
'uvicorn.access': {
57+
'handlers': ['access'],
58+
'level': 'INFO',
59+
'propagate': False,
60+
},
61+
},
62+
'root': {
63+
'level': 'INFO',
64+
'formatter': 'verbose',
65+
'handlers': LOG_DEFAULT_HANDLERS,
66+
},
67+
}

src/main.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
"""
2+
Run through console:
3+
4+
uvicorn src.main:app --host 127.0.0.1 --port 8080
5+
additional notes are in the `../README.md` file
6+
27
Routs which work "from the box":
38
49
docs_url: "/docs" - Swagger
@@ -7,13 +12,15 @@
712
"""
813
from fastapi import FastAPI
914

15+
from src.apiv1 import base as apiv1_base
1016
from src.core import config
1117

1218

1319
def get_application():
1420
_app = FastAPI(
1521
title=config.app_settings.PROJECT_NAME,
1622
)
23+
_app.include_router(apiv1_base.router, prefix='')
1724

1825
return _app
1926

src/models/base.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pydantic import BaseModel, Field, HttpUrl
2+
3+
LENGTH = 4
4+
5+
6+
class UrlModel(BaseModel):
7+
url: HttpUrl
8+
9+
10+
class IdModel(BaseModel):
11+
id: str = Field(min_length=LENGTH, max_length=LENGTH)
12+
13+
14+
class RecordModel(BaseModel):
15+
"""entity model for "database"""
16+
17+
url_id: str = Field(min_length=LENGTH, max_length=LENGTH)
18+
url_full: HttpUrl
19+
used: int = 0 # how many times this link was used
20+
deprecated: bool = False

0 commit comments

Comments
 (0)