Skip to content

Commit f6ae557

Browse files
committed
Add code for TR
1 parent 50f4b2d commit f6ae557

File tree

11 files changed

+375
-0
lines changed

11 files changed

+375
-0
lines changed

fastapi-url-shortener/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# FastAPI URL Shortener
2+
3+
## Install the Project
4+
5+
1. Create a Python virtual environment
6+
7+
```sh
8+
$ python -m venv venv
9+
$ source venv/bin/activate
10+
(venv) $
11+
```
12+
13+
2. Install the requirements
14+
15+
```
16+
(venv) $ python -m pip install -r requirements.txt
17+
```
18+
19+
## Run the Project
20+
21+
You can run the project with this command in your terminal:
22+
23+
```sh
24+
(venv) $ uvicorn shortener_app.main:app --reload
25+
```
26+
27+
Your server will reload automacially when you change a file.
28+
29+
## Verify Your Environment Variables
30+
31+
The project provides default environment settings in [`shortener_app/config.py`](shortener_app/config.py).
32+
While you can use the default settings, [it's recommended](https://12factor.net/config) to create a `.env` file to store your settings outside of your production code. E.g.:
33+
34+
```config
35+
# .env
36+
ENV_NAME="Development"
37+
BASE_URL="http://url.shortener"
38+
DB_URL="sqlite:///./test_database.db"
39+
```
40+
41+
With an `.env` file that contains the `ENV_NAME` variable with the value `"Development"` you can verify if your external `.env` file loads correctly:
42+
43+
```pycon
44+
>>> from shortener_app.config import get_settings
45+
>>> get_settings().env_name
46+
... loading Settings
47+
'Development'
48+
```
49+
50+
To get an overview of the environment variables you can set, check the [`shortener_app/config.py`](shortener_app/config.py) file.
51+
52+
> ☝️ **Note:** You should never add the `.env` file to your version control system.
53+
54+
## Visit the Documentation
55+
56+
When the project is running you can visit the documentation in your browser:
57+
58+
- http://127.0.0.1:8000/docs
59+
- http://127.0.0.1:8000/redoc
60+
61+
## About the Author
62+
63+
Philipp Acsany - Email: [email protected]
64+
65+
## License
66+
67+
Distributed under the MIT license. See `LICENSE` in the root directory of this `materials` repo for more information.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# FastAPI URL Shortener
2+
3+
## Install the Project
4+
5+
1. Create a Python virtual environment
6+
7+
```sh
8+
$ python -m venv venv
9+
$ source venv/bin/activate
10+
(venv) $
11+
```
12+
13+
2. Install the requirements
14+
15+
```
16+
(venv) $ python -m pip install -r requirements.txt
17+
```
18+
19+
## Run the Project
20+
21+
You can run the project with this command in your terminal:
22+
23+
```sh
24+
(venv) $ uvicorn shortener_app.main:app --reload
25+
```
26+
27+
Your server will reload automacially when you change a file.
28+
29+
## Verify Your Environment Variables
30+
31+
The project provides default environment settings in [`shortener_app/config.py`](shortener_app/config.py).
32+
While you can use the default settings, [it's recommended](https://12factor.net/config) to create a `.env` file to store your settings outside of your production code. E.g.:
33+
34+
```config
35+
# .env
36+
ENV_NAME="Development"
37+
BASE_URL="http://url.shortener"
38+
DB_URL="sqlite:///./test_database.db"
39+
```
40+
41+
With an `.env` file that contains the `ENV_NAME` variable with the value `"Development"` you can verify if your external `.env` file loads correctly:
42+
43+
```pycon
44+
>>> from shortener_app.config import get_settings
45+
>>> get_settings().env_name
46+
... loading Settings
47+
'Development'
48+
```
49+
50+
To get an overview of the environment variables you can set, check the [`shortener_app/config.py`](shortener_app/config.py) file.
51+
52+
> ☝️ **Note:** You should never add the `.env` file to your version control system.
53+
54+
## Visit the Documentation
55+
56+
When the project is running you can visit the documentation in your browser:
57+
58+
- http://127.0.0.1:8000/docs
59+
- http://127.0.0.1:8000/redoc
60+
61+
## About the Author
62+
63+
Philipp Acsany - Email: [email protected]
64+
65+
## License
66+
67+
Distributed under the MIT license. See `LICENSE` in the root directory of this `materials` repo for more information.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
anyio==3.5.0
2+
asgiref==3.5.0
3+
click==8.0.4
4+
decorator==5.1.1
5+
fastapi==0.75.0
6+
h11==0.13.0
7+
idna==3.3
8+
pydantic==1.9.0
9+
python-dotenv==0.19.2
10+
six==1.16.0
11+
sniffio==1.2.0
12+
SQLAlchemy==1.4.32
13+
starlette==0.17.1
14+
typing_extensions==4.1.1
15+
uvicorn==0.17.6
16+
validators==0.18.2

fastapi-url-shortener/source_code_final/shortener_app/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from functools import lru_cache
2+
3+
from pydantic import BaseSettings
4+
5+
6+
class Settings(BaseSettings):
7+
env_name: str = "Local"
8+
base_url: str = "http://localhost:8000"
9+
db_url: str = "sqlite:///./shortener.db"
10+
11+
class Config:
12+
env_file = ".env"
13+
14+
15+
@lru_cache()
16+
def get_settings() -> Settings:
17+
settings = Settings()
18+
print(f"Loading settings for: {settings.env_name}")
19+
return settings
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from sqlalchemy.orm import Session
2+
3+
from . import utils, models, schemas
4+
5+
6+
def get_db_url_by_key(db: Session, url_key: str) -> models.URL:
7+
return (
8+
db.query(models.URL)
9+
.filter(models.URL.key == url_key, models.URL.is_active)
10+
.first()
11+
)
12+
13+
14+
def create_db_url(db: Session, url: schemas.URLBase) -> models.URL:
15+
key = utils.generate_unique_key(db)
16+
secret_key = f"{key}_{utils.create_random_key(length=8)}"
17+
db_url = models.URL(
18+
target_url=url.target_url, key=key, secret_key=secret_key
19+
)
20+
db.add(db_url)
21+
db.commit()
22+
db.refresh(db_url)
23+
return db_url
24+
25+
26+
def get_db_url_by_secret_key(db: Session, secret_key: str) -> models.URL:
27+
return (
28+
db.query(models.URL)
29+
.filter(models.URL.secret_key == secret_key, models.URL.is_active)
30+
.first()
31+
)
32+
33+
34+
def update_db_clicks(db: Session, db_url: schemas.URL) -> models.URL:
35+
db_url.clicks += 1
36+
db.commit()
37+
db.refresh(db_url)
38+
return db_url
39+
40+
41+
def deactivate_db_url_by_secret_key(
42+
db: Session, secret_key: str
43+
) -> models.URL:
44+
db_url = get_db_url_by_secret_key(db, secret_key)
45+
if db_url:
46+
db_url.is_active = False
47+
db.commit()
48+
db.refresh(db_url)
49+
return db_url
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from sqlalchemy import create_engine
2+
from sqlalchemy.ext.declarative import declarative_base
3+
from sqlalchemy.orm import sessionmaker
4+
5+
from .config import get_settings
6+
7+
8+
engine = create_engine(
9+
get_settings().db_url, connect_args={"check_same_thread": False}
10+
)
11+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12+
Base = declarative_base()
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import validators
2+
3+
from fastapi import Depends, FastAPI, HTTPException, Request
4+
from fastapi.responses import RedirectResponse
5+
from sqlalchemy.orm import Session
6+
from starlette.datastructures import URL
7+
8+
from . import crud, models, schemas
9+
from .database import SessionLocal, engine
10+
from .config import get_settings
11+
12+
13+
app = FastAPI()
14+
models.Base.metadata.create_all(bind=engine)
15+
16+
17+
def get_db():
18+
db = SessionLocal()
19+
try:
20+
yield db
21+
finally:
22+
db.close()
23+
24+
25+
def raise_bad_request(message):
26+
raise HTTPException(status_code=400, detail=message)
27+
28+
29+
def raise_not_found(request):
30+
message = f"URL '{request.url}' doesn't exist"
31+
raise HTTPException(status_code=404, detail=message)
32+
33+
34+
def get_admin_info(db_url: models.URL) -> schemas.URLInfo:
35+
base_url = URL(get_settings().base_url)
36+
admin_endpoint = app.url_path_for(
37+
"administration info", secret_key=db_url.secret_key
38+
)
39+
db_url.url = str(base_url.replace(path=db_url.key))
40+
db_url.admin_url = str(base_url.replace(path=admin_endpoint))
41+
return db_url
42+
43+
44+
@app.get("/")
45+
def read_root():
46+
return "Welcome to the URL Shortener API :)"
47+
48+
49+
@app.post("/url/", response_model=schemas.URLInfo)
50+
def create_url(url: schemas.URLBase, db: Session = Depends(get_db)):
51+
if not validators.url(url.target_url):
52+
raise_bad_request(message="Your provided URL is not valid")
53+
54+
db_url = crud.create_db_url(db=db, url=url)
55+
return get_admin_info(db_url)
56+
57+
58+
@app.get("/{url_key}")
59+
def forward_to_target_url(
60+
url_key: str, request: Request, db: Session = Depends(get_db)
61+
):
62+
if db_url := crud.get_db_url_by_key(db=db, url_key=url_key):
63+
crud.update_db_clicks(db=db, db_url=db_url)
64+
return RedirectResponse(db_url.target_url)
65+
else:
66+
raise_not_found(request)
67+
68+
69+
@app.get(
70+
"/admin/{secret_key}",
71+
name="administration info",
72+
response_model=schemas.URLInfo,
73+
)
74+
def get_url_info(
75+
secret_key: str, request: Request, db: Session = Depends(get_db)
76+
):
77+
if db_url := crud.get_db_url_by_secret_key(db, secret_key=secret_key):
78+
return get_admin_info(db_url)
79+
else:
80+
raise_not_found(request)
81+
82+
83+
@app.delete("/admin/{secret_key}")
84+
def delete_url(
85+
secret_key: str, request: Request, db: Session = Depends(get_db)
86+
):
87+
if db_url := crud.deactivate_db_url_by_secret_key(
88+
db, secret_key=secret_key
89+
):
90+
message = (
91+
f"Successfully deleted shortened URL for '{db_url.target_url}'"
92+
)
93+
return {"detail": message}
94+
else:
95+
raise_not_found(request)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from sqlalchemy import Boolean, Column, Integer, String
2+
3+
from .database import Base
4+
5+
6+
class URL(Base):
7+
__tablename__ = "urls"
8+
9+
id = Column(Integer, primary_key=True)
10+
key = Column(String, unique=True, index=True)
11+
secret_key = Column(String, unique=True, index=True)
12+
target_url = Column(String, index=True)
13+
is_active = Column(Boolean, default=True)
14+
clicks = Column(Integer, default=0)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from pydantic import BaseModel
2+
3+
4+
class URLBase(BaseModel):
5+
target_url: str
6+
7+
8+
class URL(URLBase):
9+
is_active: bool
10+
clicks: int
11+
12+
class Config:
13+
orm_mode = True
14+
15+
16+
class URLInfo(URL):
17+
url: str
18+
admin_url: str

0 commit comments

Comments
 (0)