Skip to content

Commit

Permalink
Merge pull request #20 from SoleSearch-Demos/main
Browse files Browse the repository at this point in the history
  • Loading branch information
peterrauscher authored Mar 6, 2024
2 parents 87c8aa4 + 57571ae commit 7fc51f4
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 31 deletions.
6 changes: 3 additions & 3 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
SOLESEARCH_DEFAULT_LIMIT=20
# The maximum number of sneakers to return in a single request
SOLESEARCH_MAX_LIMIT=100
# The default number of sneakers to skip in a single request
SOLESEARCH_DEFAULT_OFFSET=0
# ====================

# === MongoDB ===
Expand All @@ -21,5 +19,7 @@ SOLESEARCH_DB_PRIMARY_COLLECTION=sneakers
SOLESEARCH_STOCKX_API_KEY=
SOLESEARCH_STOCKX_CLIENT_ID=
SOLESEARCH_STOCKX_CLIENT_SECRET=
SOLESEARCH_STOCKX_CALLBACK_URL=https://localhost:3000/auth/stockx
SOLESEARCH_STOCKX_CALLBACK_URL=https://localhost:8000/auth/stockx
SOLESEARCH_STOCKX_CALLBACK_STATE=
SOLESEARCH_SESSION_SECRET=
# ===================
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ By default, all products in the database are returned when calling `/sneakers`.
- releaseDate
- released

Filtering is case insensitive, but the search will start from the beginning of each field. For example, a search `/sneakers?brand=jordan` will return all sneakers with a brand starting with `Jordan`, `jordan`, `JOrdan`, etc... but a search `/sneakers?brand=ordan` will not find any `Jordan`s.
Filtering is case insensitive, but the search will always start at the beginning of each field. For example, a search `/sneakers?brand=jordan` will return all sneakers with a brand starting with `Jordan`, `jordan`, `JORdan`, etc... but a search `/sneakers?brand=ordan` will not find any `Jordan` brand sneakers. For this reason, those needing more comprehensive "search" functionality should use the `/search` endpoint.

### Sorting

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ install-name = "api"
sources = ["src/api"]

[tool.hatch.build.targets.zipped-directory.force-include]
".env" = "/.env"
".env" = "/.env"
"static" = "/static"
6 changes: 3 additions & 3 deletions src/api/data/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from enum import Enum
from typing import List

from beanie import Document
from pydantic import BaseModel

from beanie import Document
from core.models.shoes import Sneaker
from core.models.shoes import Sneaker, SneakerView


class Token(Document):
Expand All @@ -21,7 +21,7 @@ class PaginatedSneakersResponse(BaseModel):
pageSize: int
nextPage: str | None
previousPage: str | None
items: List[Sneaker]
items: List[SneakerView]


class SortKey(str, Enum):
Expand Down
Binary file removed src/api/favicon.ico
Binary file not shown.
27 changes: 22 additions & 5 deletions src/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
load_dotenv(os.path.join(os.getcwd(), ".env"))

from beanie import init_beanie
from core.models.shoes import Sneaker
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.staticfiles import StaticFiles
from mangum import Mangum
from starlette.middleware.sessions import SessionMiddleware

from api.data.instance import DATABASE_NAME, client
from api.routes import auth, sneakers
from core.models.shoes import Sneaker

desc = """
### The Bloomberg Terminal of Sneakers
Expand All @@ -27,14 +29,18 @@
"""

app = FastAPI(
redoc_url=None, # Disable redoc, keep only swagger
redoc_url=None,
docs_url=None,
title="SoleSearch",
version=__version__,
contact={"name": "SoleSearch Developer Support", "email": "[email protected]"},
contact={"name": "SoleSearch Email Support", "email": "[email protected]"},
description=desc,
responses={404: {"description": "Not found"}}, # Custom 404 page
)

# Serve static files from the /static directory
app.mount("/static", StaticFiles(directory="static"), name="static")

# Enable CORS
app.add_middleware(
CORSMiddleware,
Expand All @@ -43,7 +49,8 @@
allow_headers=["*"],
)
# Enable session handling for StocxkX OAuth flow
app.add_middleware(SessionMiddleware, secret_key="vT!y!r5s#bwcDxDG")
SESSION_SECRET = os.environ.get("SOLESEARCH_SESSION_SECRET", "this should be a secret")
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)


@app.on_event("startup")
Expand All @@ -58,13 +65,23 @@ async def startup_event():
app.include_router(auth.router)


@app.get("/docs", include_in_schema=False)
async def swagger_ui_html():
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title=app.title + " - Documentation",
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
swagger_favicon_url="/static/favicon.png",
)


# This is the entry point for AWS Lambda
handler = Mangum(app)

if __name__ == "__main__":
import uvicorn

# Run the app locally using Uvicorn
# Run the app locally using Uvicorn, with SSL enabled
uvicorn.run(
app,
host="localhost",
Expand Down
7 changes: 5 additions & 2 deletions src/api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
STOCKX_CLIENT_ID = os.environ.get("SOLESEARCH_STOCKX_CLIENT_ID", None)
STOCKX_CLIENT_SECRET = os.environ.get("SOLESEARCH_STOCKX_CLIENT_SECRET", None)
STOCKX_API_KEY = os.environ.get("SOLESEARCH_STOCKX_API_KEY", None)
STOCKX_STATE = os.environ.get(
"SOLESEARCH_STOCKX_CALLBACK_STATE", "this should be a secret string"
)

session = requests.session()

Expand All @@ -22,7 +25,7 @@

@router.get("/stockx")
async def login_via_stockx(state: str, request: Request):
if state != "YTPc2DqAwnmhHGzSQVtzwEPq2eEgprUi":
if state != STOCKX_STATE:
raise HTTPException(status_code=400, detail="Bad state. Nice try, buster.")
auth_url = "https://accounts.stockx.com/authorize"
print(STOCKX_CLIENT_ID, STOCKX_CLIENT_SECRET, STOCKX_API_KEY)
Expand All @@ -42,7 +45,7 @@ async def login_via_stockx(state: str, request: Request):
async def stockx_oauth_callback(state: str, code: str, request: Request):
if code is None:
raise HTTPException(status_code=400, detail="No code returned from StockX.")
if state != "YTPc2DqAwnmhHGzSQVtzwEPq2eEgprUi":
if state != STOCKX_STATE:
raise HTTPException(status_code=400, detail="Bad state. Nice try, buster.")
try:
headers = {
Expand Down
Empty file added src/api/routes/search.py
Empty file.
93 changes: 79 additions & 14 deletions src/api/routes/sneakers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import os
from datetime import UTC, datetime
import os
from typing import Annotated

from core.models.details import Audience
from core.models.shoes import Sneaker
from fastapi import APIRouter, HTTPException, Query, Request

from api.data.models import PaginatedSneakersResponse, SortKey, SortOrder
from api.util import url_for_query
from core.models.details import Audience
from core.models.shoes import Sneaker, SneakerView

router = APIRouter(
prefix="/sneakers",
Expand All @@ -21,16 +20,79 @@
@router.get("/")
async def get_sneakers(
request: Request,
brand: str | None = None,
name: str | None = None,
colorway: str | None = None,
audience: Audience | None = None,
releaseDate: str | None = None,
released: bool | None = None,
sort: SortKey = SortKey.RELEASE_DATE,
order: SortOrder = SortOrder.DESCENDING,
page: Annotated[int | None, Query(gte=1)] = None,
pageSize: Annotated[int | None, Query(gte=1, lte=MAX_LIMIT)] = None,
brand: Annotated[
str | None,
Query(
title="Brand", description="Filter by the brand of the shoes.", min_length=3
),
] = None,
name: Annotated[
str | None,
Query(
title="Product Name",
description="Filter by the name of the shoes.",
min_length=3,
),
] = None,
colorway: Annotated[
str | None,
Query(
title="Colorway",
description="Filter by the colorway of the shoes.",
min_length=3,
),
] = None,
audience: Annotated[
Audience | None,
Query(
title="Audience",
description="Filter on the gender/audience of the shoes. See Audience for possible values.",
min_length=3,
),
] = None,
releaseDate: Annotated[
str | None,
Query(
title="Release Date",
description="Filter by the release date of the shoes. Can be a specific date or an inequality. Operators are (lt, lte, gt, gte). Example usage: lt:2021-01-01",
),
] = None,
released: Annotated[
bool | None,
Query(
title="Released?",
description="Filter by whether the shoes have been released or not. Overrides any filter on releaseDate if set.",
),
] = None,
sort: Annotated[
SortKey,
Query(
title="Sort By",
description="The field to sort by.",
),
] = SortKey.RELEASE_DATE,
order: Annotated[
SortOrder,
Query(
title="Sort Order",
description="The order to sort in based on the sort key.",
),
] = SortOrder.DESCENDING,
page: Annotated[
int | None,
Query(
gte=1, title="Page Number", description="The page number of the result set."
),
] = None,
pageSize: Annotated[
int | None,
Query(
gte=1,
lte=MAX_LIMIT,
title="Page Size",
description=f"The number of items on each page. Must be in the range [1-{MAX_LIMIT}] (inclusive).",
),
] = None,
) -> PaginatedSneakersResponse:
query = Sneaker.find()
if not page:
Expand All @@ -44,7 +106,9 @@ async def get_sneakers(
if colorway:
query = query.find({"colorway": {"$regex": f"^{colorway}", "$options": "i"}})
if audience:
query = query.find({"audience": {"$regex": f"^{audience.value}"}})
query = query.find(
{"audience": {"$regex": f"^{audience.value}", "$options": "i"}}
)
if released is not None:
now = datetime.now(UTC)
if released:
Expand Down Expand Up @@ -82,6 +146,7 @@ async def get_sneakers(
await query.sort(f"{'+' if order == SortOrder.ASCENDING else '-'}{sort.value}")
.skip((page - 1) * pageSize)
.limit(pageSize)
.project(SneakerView)
.to_list()
)
result = PaginatedSneakersResponse(
Expand Down
4 changes: 2 additions & 2 deletions src/api/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from fastapi import Request


def url_for_query(request: Request, name: str, **params: str) -> str:
url = str(request.url_for(name))
def url_for_query(request: Request, fastapi_function_name: str, **params: str) -> str:
url = str(request.url_for(fastapi_function_name))
parsed = urlparse(url)
parsed = parsed._replace(query=urlencode(params))
return urlunparse(parsed)
Binary file added static/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7fc51f4

Please sign in to comment.