Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data reduction: Send image url instead of image data on search #2

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from sqlalchemy import pool

from alembic import context
from app.orm import base

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand All @@ -20,7 +21,7 @@
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
target_metadata = base.BaseOrm.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""add raw img column to creatures table

Revision ID: 1a4804acc824
Revises: 85bc789138fe
Create Date: 2021-10-11 04:24:03.926897

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.sql import table, column
from sqlalchemy import String
from sqlalchemy.sql.expression import select
from sqlalchemy.sql.sqltypes import Integer, LargeBinary

from app.orm.creature import convert_from_base64_img_tag_data

# revision identifiers, used by Alembic.
revision = '1a4804acc824'
down_revision = '85bc789138fe'
branch_labels = None
depends_on = None
import logging
LOG = logging.getLogger(__file__)

RAW_COLUMN = 'battle_sprite_raw'
B64_COLUMN = 'battle_sprite'

def upgrade():
bind = op.get_bind()
session = sa.orm.Session(bind=bind)
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('creatures', sa.Column(RAW_COLUMN, sa.LargeBinary(), nullable=True))
# ### end Alembic commands ###
# decode base64 image back into png
creature = table('creatures',column('id', Integer), column(B64_COLUMN, String), column(RAW_COLUMN, LargeBinary))

results = session.execute(select(creature.c.id, creature.c.battle_sprite))
for (id_, data) in results:
png = convert_from_base64_img_tag_data(data)
session.execute(creature
.update()
.where(creature.c.id == id_)
.values(battle_sprite_raw=png,)
)

op.alter_column('creatures', RAW_COLUMN, nullable=False)



def downgrade():
op.drop_column('creatures', RAW_COLUMN)
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""add raw image column to klass table

Revision ID: 93edf5b21ef6
Revises: 1a4804acc824
Create Date: 2021-10-11 21:47:09.420963

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.sql.expression import column, select, table
from sqlalchemy.sql.sqltypes import Integer, LargeBinary, String
from sqlalchemy.sql.type_api import INTEGERTYPE

from app.orm.creature import convert_from_base64_img_tag_data

# revision identifiers, used by Alembic.
revision = '93edf5b21ef6'
down_revision = '1a4804acc824'
branch_labels = None
depends_on = None

RAW_COLUMN = 'icon_raw'
B64_COLUMN = 'icon'

def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('klasses', sa.Column('icon_raw', sa.LargeBinary(), nullable=True))
# ### end Alembic commands ###
bind = op.get_bind()
session = sa.orm.Session(bind=bind)


klass = table('klasses',column('id', Integer), column(B64_COLUMN, String), column(RAW_COLUMN, LargeBinary))

results = session.execute(select(klass.c.id, klass.c.icon))
for (id_, data) in results:
png = convert_from_base64_img_tag_data(data)
session.execute(klass
.update()
.where(klass.c.id == id_)
.values(icon_raw=png,)
)

op.alter_column('klasses', RAW_COLUMN, nullable=False)


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('klasses', 'icon_raw')
# ### end Alembic commands ###
8 changes: 6 additions & 2 deletions app/models/creature.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations
from datetime import datetime

from typing import List, Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field

from .base import BaseModelOrm
from .klass import KlassModel
Expand All @@ -16,7 +17,8 @@ class CreatureModel(BaseModel, BaseModelOrm):
slug: str
description: Optional[str] = None

battle_sprite: str
# url for battle sprite
battle_sprite_url: str = Field(None, alias="battle_sprite")

health: int
attack: int
Expand All @@ -34,3 +36,5 @@ class CreatureModel(BaseModel, BaseModelOrm):

class Config:
orm_mode = True
# The alias field must not ever be supplied else its contents will be used instead
allow_population_by_field_name = True
5 changes: 4 additions & 1 deletion app/models/klass.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Optional
from pydantic import BaseModel
from pydantic.fields import Field

from .base import BaseModelOrm

Expand All @@ -13,10 +14,12 @@ class KlassModel(BaseModel, BaseModelOrm):
description: Optional[str] = None

color: str
icon: str
icon_url: str = Field(None, alias="icon")

created_at: datetime
updated_at: datetime

class Config:
orm_mode = True
# The alias field must never be supplied else its contents will be used instead
allow_population_by_field_name = True
5 changes: 4 additions & 1 deletion app/models/race.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Optional
from pydantic import BaseModel
from pydantic.fields import Field

from .base import BaseModelOrm
from .klass import KlassModel
Expand All @@ -13,11 +14,13 @@ class RaceModel(BaseModel, BaseModelOrm):
slug: str
description: Optional[str] = None

icon: str
icon_url: str = Field(None, alias="icon")
default_klass: KlassModel

created_at: datetime
updated_at: datetime

class Config:
orm_mode = True
# The alias field must not ever be supplied else its contents will be used instead
allow_population_by_field_name = True
24 changes: 23 additions & 1 deletion app/orm/creature.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import base64
from sqlalchemy import Column, Integer, String, ForeignKey, Text, TIMESTAMP
from sqlalchemy.orm import relationship
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.sql.sqltypes import LargeBinary
from sqlalchemy.util.langhelpers import hybridproperty

from .base import BaseOrm, build_slug_defaulter, FullText


def default_img_data(context) -> bytes:
b64 = context.get_current_parameters()["battle_sprite"]
return convert_from_base64_img_tag_data(b64)


def convert_from_base64_img_tag_data(b64: str) -> bytes:
# get the base64 data only, chop off web type info
data = b64.split("data:image/png;base64,")[1]
return base64.b64decode(data)


class CreatureOrm(BaseOrm):
__tablename__ = "creatures"

Expand All @@ -21,7 +35,15 @@ class CreatureOrm(BaseOrm):
)
description = Column(Text())

battle_sprite = Column(Text(), nullable=False)
battle_sprite_base64 = Column("battle_sprite", Text(), nullable=False)
battle_sprite_raw = Column(
LargeBinary, nullable=False, default=default_img_data
)

@hybridproperty
def battle_sprite_url(self):
val = f"/api/creatures/{self.id}/images/battle_sprite.png"
return val

health = Column("health", Integer, nullable=False)
attack = Column("attack", Integer, nullable=False)
Expand Down
18 changes: 17 additions & 1 deletion app/orm/klass.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
from sqlalchemy.sql.sqltypes import LargeBinary
from sqlalchemy.util.langhelpers import hybridproperty

from app.orm.creature import convert_from_base64_img_tag_data
from .base import BaseOrm, build_slug_defaulter, FullText


def default_img_data(context) -> bytes:
b64 = context.get_current_parameters()["icon"]
return convert_from_base64_img_tag_data(b64)


class KlassOrm(BaseOrm):
__tablename__ = "klasses"

Expand All @@ -20,7 +28,15 @@ class KlassOrm(BaseOrm):
description = Column(Text())

color = Column(String(10), nullable=False)
icon = Column(Text(), nullable=False)
icon_b64 = Column("icon", Text(), nullable=False)
icon_raw = Column(
"icon_raw", LargeBinary, nullable=False, default=default_img_data
)

@hybridproperty
def icon_url(self):
val = f"/api/classes/{self.id}/images/icon.png"
return val

created_at = Column("created_at", TIMESTAMP, nullable=False)
updated_at = Column("updated_at", TIMESTAMP, nullable=False)
18 changes: 17 additions & 1 deletion app/orm/race.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
from sqlalchemy import Column, Integer, String, Text, ForeignKey, TIMESTAMP
from sqlalchemy.orm import relationship
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.sql.sqltypes import LargeBinary
from sqlalchemy.util.langhelpers import hybridproperty

from .base import BaseOrm, build_slug_defaulter, FullText
from app.orm.creature import convert_from_base64_img_tag_data


def default_img_data(context) -> bytes:
b64 = context.get_current_parameters()["icon"]
return convert_from_base64_img_tag_data(b64)


class RaceOrm(BaseOrm):
Expand All @@ -21,7 +29,15 @@ class RaceOrm(BaseOrm):
)
description = Column(Text())

icon = Column(Text(), nullable=False)
icon_b64 = Column("icon", Text(), nullable=False)
icon_raw = Column(
"icon_raw", LargeBinary, nullable=False, default=default_img_data
)

@hybridproperty
def icon_url(self):
val = f"/api/races/{self.id}/images/icon.png"
return val

default_klass_id = Column(
Integer, ForeignKey("klasses.id"), nullable=False
Expand Down
23 changes: 23 additions & 0 deletions app/routers/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy import func
from starlette.responses import Response

from app.orm.klass import KlassOrm
from app.models.klass import KlassModel
Expand Down Expand Up @@ -123,3 +124,25 @@ def get(klass_id: str, session=Depends(has_session)):
)
klasses_model = KlassModel.from_orm(klasses_orm)
return KlassesGetSchema(data=klasses_model)


@router.get(
"/{klass_id}/images/icon.png",
# Set what the media type will be in the autogenerated OpenAPI specification.
# fastapi.tiangolo.com/advanced/additional-responses/#additional-media-types-for-the-main-response
responses={200: {"content": {"image/png": {}}}},
# Prevent FastAPI from adding "application/json" as an additional
# response media type in the autogenerated OpenAPI specification.
# https://github.com/tiangolo/fastapi/issues/3258
response_class=Response,
)
def fetch_class_icon(klass_id: int, session=Depends(has_session)):
png = (
select(KlassOrm.icon_raw)
.where(KlassOrm.id == klass_id)
.get_scalar(session)
)

headers = {"Cache-Control": "public,max-age=604800,immutable"}

return Response(content=png, media_type="image/png", headers=headers)
23 changes: 23 additions & 0 deletions app/routers/creatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic import BaseModel
from sqlalchemy.orm import contains_eager, selectinload
from sqlalchemy import func
from starlette.responses import Response

from app.orm.creature import CreatureOrm
from app.models.creature import CreatureModel
Expand Down Expand Up @@ -113,6 +114,28 @@ class CreaturesSearchRequest(BaseModel):
sorting: Optional[SortingRequestSchema] = SortingRequestSchema()


@router.get(
"/{creature_id}/images/battle_sprite.png",
# Set what the media type will be in the autogenerated OpenAPI specification.
# fastapi.tiangolo.com/advanced/additional-responses/#additional-media-types-for-the-main-response
responses={200: {"content": {"image/png": {}}}},
# Prevent FastAPI from adding "application/json" as an additional
# response media type in the autogenerated OpenAPI specification.
# https://github.com/tiangolo/fastapi/issues/3258
response_class=Response,
)
def fetch_creature_sprite(creature_id: int, session=Depends(has_session)):
png = (
select(CreatureOrm.battle_sprite_raw)
.where(CreatureOrm.id == creature_id)
.get_scalar(session)
)

headers = {"Cache-Control": "public,max-age=604800,immutable"}

return Response(content=png, media_type="image/png", headers=headers)


@router.post("/search", response_model=CreaturesSearchSchema)
def search(search: CreaturesSearchRequest, session=Depends(has_session)):
creatures_count = (
Expand Down
Loading