Add Favicon to WordPress

요즘 워드프레스 템플릿이 뭐하나 바꾸기가 참 어렵게 되어있죠? 파비콘을 하나 넣으려니 참 이게 안되네요. 그래서 그냥 코드에 박아버렸습니다. 권장하는 방법은 아니에요. 워드프레스 업그레이드하면 지워지거든요. 워드프레스 템플릿만들기 강좌 끝나면 적합한 방법을 다시 알려드리겠습니다. 이건 그냥 임시방편으로 사용하실수 있는 코딩이에요. 일단 워드프레스 사이트를 파일매니저로 여시면 wp-includes라는 폴더가 있을거에요. 그 안에 template-canvas.php라는 파일을 여세요. 그러면 아래와 같이 <link rel="shortcut icon" href="favicon.ico">태그를 넣으셔서 파비콘을 임시로 보여주게 만들수 있어요. 물론 루트폴더에 favicon.ico을 넣으셔야 나오겠죠?

<?php
/**
 * Template canvas file to render the current 'wp_template'.
 *
 * @package WordPress
 */

/*
 * Get the template HTML.
 * This needs to run before <head> so that blocks can add scripts and styles in wp_head().
 */
$template_html = get_the_block_template_html();
?><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
        <meta charset="<?php bloginfo( 'charset' ); ?>" />
        <link rel="shortcut icon" href="favicon/favicon.ico">
        <?php wp_head(); ?>
</head>

<body <?php body_class(); ?>>
<?php wp_body_open(); ?>

<?php echo $template_html; ?>

<?php wp_footer(); ?>
</body>
</html>

SQLAlchemy – Select Data

시작하기에 앞서 데이타베이스 연결을 우선적으로 합니다.

from sqlalchemy import create_engine
from sqlalchemy import text

uri = 'mysql://DB_USER:DB_PASS@DB_HOST/DB_NAME'

engine = create_engine(uri, echo=False)

코드를 실행하다보면 connection이 끊어질때가 있는데 그때마다 engine을 다시 연결해주시면 됩니다.

그리고 이전시간에 사용했던 테이블을 계속해서 사용할 건데요. 테이블이 데이타베이스에 없으신 분들은 아래 코드를 실행하여 생성해주시기 바랍니다.

from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy import ForeignKey
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(30), nullable=False)
    fullname = mapped_column(String(30))
    addresses = relationship("Address", back_populates="user")

    def __repr__(self) -> str:
        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

class Address(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user_account.id"))
    email_address = mapped_column(String(30), nullable=True)
    user = relationship("User", back_populates="addresses")

    def __repr__(self) -> str:
        return f"Address(id={self.id!r}, user_id={self.user_id!r}, email_address={self.email_address!r})"

# 테이블 생성
Base.metadata.create_all(engine)

# 생성한 테이블 삭제
Base.metadata.drop_all(engine)

select() SQL 표현식 구조

select문도 insert와 마찬가지로 statement을 먼저 만든후에 실행을 하게 되는데요. select()함수의 statement를 보시면 아래와 같습니다. 아래는 이름이 “spongebob”인 사용자의 레코드를 가져오는 코드입니다.

from sqlalchemy import select
stmt = select(User).where(User.name == "spongebob")

>>> print(stmt)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = :name_1

위의 User는 ORM으로 정의 되었지만 ORM없이 mapper()를 통하여 mapping을 하는 경우 Table()로 정의된 변수로 대체하여 실행해도 같은 결과가 나옵니다. 다만 Table객체는 칼럼이 .c안에 들어가 있으므로 Table.c.칼럼명 이렇게 해 주셔야 에러없이 실행이 됩니다. 결과는 ORM통해서 실행했을때와 완전 동일하게 나옵니다.

from sqlalchemy import select
stmt = select(user_table).where(user_table.c.name == "spongebob")

>>> print(stmt)
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account
WHERE user_account.name = :name_1

위와 같이 statement을 만들고나서 engine에 connect해서 해당 statement을 execute()함수로 실행을 시켜야 결과를 반환합니다.

with engine.connect() as conn:
    for row in conn.execute(stmt):
        print(row)

위와 같은 코드를 실행하면 아래와 같이 spongebob의 레코드를 볼수 있습니다. Model은 mapped_column이나 relationship또는 DeclarativeBase등을 사용하여 베이스를 만들었지만 사실상 ORM의 Session으로 실행하지 않으면 온전히 ORM을 사용한다고 볼수가 없습니다. 그런 의미에서 아래 나오는 결과는 쿼리의 결과이지 ORM이 넘겨주는 객체의 결과물은 아니에요.

(1, 'spongebob', 'Spongebob Squarepants')

ORM을 사용할때는 engine에서 connect를 하지 않고, Session을 통해서 쿼리를 하는데요.

from sqlalchemy.orm import Session

stmt = select(User).where(User.name == "spongebob")
with Session(engine) as session:
    for obj_user in session.execute(stmt):
        print(obj_user)

위와 같이 Session을 사용하는 경우 반환되는 레코드가 User모델객체이기 때문에 print를 했을때 위에서 선언한 User.__repr__()함수에서 정의한 대로 결과를 출력하니까 여기서 보여주지 않아도 되는 필드는 빼거나 보여주고 싶은 항목은 추가하시면 됩니다. 위의 ORM 코드를 실행하면 다음과 같습니다.

(User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)

검색필드 지정 및 가져올 테이블 설정

select()함수는 해당 모델의 데이타를 불러오는 기능을 합니다. 기본쿼리를 보면 다음과 같습니다.

>>> print(select(User))
SELECT user_account.id, user_account.name, user_account.fullname
FROM user_account

select에 인자로 필드명을 나열하면 명시한 필드만 불러오도록 쿼리를 생성합니다.

>>> print(select(User.name, User.fullname))
SELECT user_account.name, user_account.fullname
FROM user_account

실제로 쿼리를 실행해보겠습니다

from sqlalchemy.orm import Session

with Session(engine) as session:
    row = session.execute(select(User)).first()
    print(row)

위의 코드를 실행하면 다음과 같습니다.

(User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)

검색결과가 배열로 반환되기 때문에 User객체 하나만 보고 싶다면 배열의 첫번째 항목을 읽어오면 됩니다.

>>> row[0]
User(id=1, name='spongebob', fullname='Spongebob Squarepants')

To be continued…사장님이 일시켜서 가봐야대요….

Resources

SQLAlchemy – Insert Data

시작하기에 앞서 데이타베이스 연결을 우선적으로 합니다.

from sqlalchemy import create_engine
from sqlalchemy import text

uri = 'mysql://DB_USER:DB_PASS@DB_HOST/DB_NAME'

engine = create_engine(uri, echo=False)

코드를 실행하다보면 connection이 끊어질때가 있는데 그때마다 engine을 다시 연결해주시면 됩니다.

그리고 이전시간에 사용했던 테이블을 계속해서 사용할 건데요. 테이블이 데이타베이스에 없으신 분들은 아래 코드를 실행하여 생성해주시기 바랍니다.

from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy import ForeignKey
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(30), nullable=False)
    fullname = mapped_column(String(30))
    addresses = relationship("Address", back_populates="user")

    def __repr__(self) -> str:
        return f"User(id={self.id!r})"

class Address(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user_account.id"))
    email_address = mapped_column(String(30), nullable=True)
    user = relationship("User", back_populates="addresses")

    def __repr__(self) -> str:
        return f"Address(id={self.id!r})"

# 테이블 생성
Base.metadata.create_all(engine)

# 생성한 테이블 삭제
Base.metadata.drop_all(engine)

INSERT 문 사용하기

insert를 구현하는 가장 간단한 방법은 insert()함수를 사용하는 것입니다.

from sqlalchemy import insert

stmt = insert(User).values(name="spongebob", fullname="Spongebob Squarepants")

위에 코드는 insert를 실행한것은 아니구요. Insert를 하는 명령어를 stmt에 저장한 거에요. 출력해보면 다음과 같습니다.

>>> print(stmt)
INSERT INTO user_account (name, fullname) VALUES (:name, :fullname)

위의 statement을 컴파일하면 다음과 같이 넘겨주는 매개변수들을 parsing할 수 있습니다.

>>> compiled = stmt.compile()
>>> compiled.params
{'name': 'spongebob', 'fullname': 'Spongebob Squarepants'}

정의된 statement을 실행하여 데이타를 입력하려면 execute()함수를 실행합니다.

with engine.connect() as conn:
    result = conn.execute(stmt)
    conn.commit()

실행 후 아래 명령을 실행하면 방금 실행한 레코드의 PK를 가져올수 있습니다.

>>> result.inserted_primary_key
(1,)

해당 테이블의 Insert문을 생성하는 방법은 아래와 같습니다.

>>> print(insert(User))
INSERT INTO user_account (id, name, fullname) VALUES (:id, :name, :fullname)

한번에 여러개의 행을 삽입하고 싶다면 아래의 코드와 같이 배열안에 값을 넣으시면 됩니다.

with engine.connect() as conn:
    result = conn.execute(
        insert(User),
        [
            {"name": "sandy", "fullname": "Sandy Cheeks"},
            {"name": "patrick", "fullname": "Patrick Star"},
        ],
    )
    conn.commit()

이번에는 User의 name을 username에 바인드를 해서 Address에 데이타를 넣는데, 이름 데이타만 넣어도 User.id를 자동으로 찾아와서 데이타를 넣어주는 고급기술을 써보도록 하겠습니다.

from sqlalchemy import select, bindparam

scalar_subq = (
    select(User.id)
    .where(User.name == bindparam("username"))
    .scalar_subquery()
)

with engine.connect() as conn:
    result = conn.execute(
        insert(Address).values(user_id=scalar_subq),
        [
            {
                "username": "spongebob",
                "email_address": "spongebob@sqlalchemy.org",
            }, {
                "username": "sandy",
                "email_address": "sandy@sqlalchemy.org",
            }, {
                "username": "sandy",
                "email_address": "sandy@squirrelpower.org",
            },
        ],
    )
    conn.commit()

이렇게 하면 scalar_subq가 서브쿼리로 들어가서 User.id를 가져오는 아래와 같은 쿼리를 생성하여 실행합니다.

BEGIN (implicit)
INSERT INTO address (user_id, email_address) VALUES ((SELECT user_account.id
FROM user_account
WHERE user_account.name = ?), ?)
[...] [('spongebob', 'spongebob@sqlalchemy.org'), ('sandy', 'sandy@sqlalchemy.org'),
('sandy', 'sandy@squirrelpower.org')]
COMMIT

이건 별로 중요한건 아닌데 참고로, insert()에 values를 아무것도 넣지않으면 필드와 데이타가 모두 비어있는 Insert문을 반환합니다. 필요한 경우 유용할수도 있겠죠.

>>> print(insert(User).values().compile(engine))
INSERT INTO user_account () VALUES ()

Insert후에 반환되는 값 사용하기

insert문을 만들때 아래와 같이 returning함수를 붙여서 그 안에 반환받을 필드를 나열하면

insert_stmt = insert(Address).returning(
    Address.id, Address.email_address
)

아래와 같이 쿼리를 만들어 주고 쿼리 실행 후에 반환되는 값을 사용할 수 있습니다.

>>> print(insert_stmt)
INSERT INTO address (id, user_id, email_address) VALUES (:id, :user_id, :email_address) RETURNING address.id, address.email_address

Insert.from_select()를 이용하면 Select해온 결과를 Insert할 수도 있습니다.

select_stmt = select(User.id, User.name + "@aol.com")
insert_stmt = insert(Address).from_select(
    ["user_id", "email_address"], select_stmt
)
>>> print(insert_stmt.returning(Address.id, Address.email_address))
INSERT INTO address (user_id, email_address) SELECT user_account.id, user_account.name || :name_1 AS anon_1
FROM user_account RETURNING address.id, address.email_address

Resources

SQLAlchemy Unified Tutorial

외국문서 보면 말이 거창해서 겁부터 나는데요. 알고 보면 너무 당연하고 상당히 쉬운 컨셉의 내용들이 참 많습니다. 여러분께 조금이나마 도움이 되는 문서가 되길 바랍니다. 본 문서는 SQLAlchemy매뉴얼의 SQLAlchemy 1.4 / 2.0 튜토리얼의 일부를 발췌하여 번역한 내용입니다.

Establishing Connectivity – the Engine

기본적으로 가지고 계신 DB에 연결을 우선 먼저 할게요.

from sqlalchemy import create_engine
from sqlalchemy import text

uri = 'mysql://DB_USER:DB_PASS@DB_HOST/DB_NAME'

engine = create_engine(uri, echo=False)

# 커넥션 테스트
with engine.connect() as conn:
    result = conn.execute(text("select 'hello world'"))
    print(result.all())

Working with Transactions and the DBAPI

연결이 잘 되었으면 테스트로 테이블을 하나 만들어 볼게요. DDL은 수동으로 커밋하도록 합니다.

with engine.connect() as conn:
    conn.execute(text("CREATE TABLE some_table (x int, y int)"))
    conn.execute(
        text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
        [{"x": 1, "y": 1}, {"x": 2, "y": 4}],
    )
    conn.commit()

아래는 자동커밋의 예제입니다. begin()함수를 사용하면 DML은 commit()함수의 호출없이도 자동으로 커밋이 되어 데이타가 삽입이됩니다. DDL은 여전히 수동으로 커밋을 해야한다네요.

with engine.begin() as conn:
    conn.execute(
        text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
        [{"x": 6, "y": 8}, {"x": 9, "y": 10}],
    )

아래는 데이타를 가져와서 속성이름으로 데이타에 접근하는 예제입니다.

with engine.connect() as conn:
    result = conn.execute(text("SELECT x, y FROM some_table"))
    for row in result:
        print(f"x: {row.x}  y: {row.y}")

for문에서 변수명을 지정해서 바로 쓸수 있게 할수 있지만, 하지만 두개 이상일때는 곤란해지기 때문에 잘 사용하지 않습니다.

with engine.connect() as conn:
    result = conn.execute(text("SELECT x, y FROM some_table"))
    for x, y in result:
        print(f"x: {x}  y: {y}")

아래는 배열방 번호로 접근하는 예제입니다. 하지만 우리가 코딩할때 배열방 번호를 일일이 기억하고 있을 수는 없지요.

with engine.connect() as conn:
    result = conn.execute(text("SELECT x, y FROM some_table"))
    for row in result:
        print(f"x: {row[0]}  y: {row[1]}")

그래서 사용하는 것이 mappings()입니다. 매핑은 필드명으로 접근하기 때문에 정확도가 높고 편리합니다.

with engine.connect() as conn:
    result = conn.execute(text("select x, y from some_table"))

    for dict_row in result.mappings():
        x = dict_row["x"]
        y = dict_row["y"]
        print(f"x: {x}  y: {y}")

Executing with an ORM Session

SQLAlchemy의 가장 상층에 존재하는 ORM(Object Relational Mapper)는 Python에서 데이터베이스를 사용하는데 필요한 많은 기능을 가지고 있는 종합선물세트 같은 존재입니다. 일부만 발췌하여 사용할 수도 있고, 종합적으로 사용할 수도 있습니다. 아래 그림은 ORM의 종속성을 보여주는 구성 계층도입니다.

아래는 ORM의 Session을 사용하여 쿼리를 해오는 예제입니다.

from sqlalchemy.orm import Session

stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y")
with Session(engine) as session:
    result = session.execute(stmt, {"y": 6})
    for row in result:
        print(f"x: {row.x}  y: {row.y}")

ORM사용시에도 UPDATE를 할때는 commit()을 명시적으로 호출하여 변경된 내용을 데이타베이스에 적용합니다.

with Session(engine) as session:
    result = session.execute(
        text("UPDATE some_table SET y=:y WHERE x=:x"),
        [{"x": 9, "y": 11}, {"x": 13, "y": 15}],
    )
    session.commit()

Working with Database Metadata

아래는 MetaData 선언하여 user테이블을 매핑하는 코드입니다. Table()에 칼럼정보를 저장하고, 모델에는 순수하게 필요한 기능만 나열한 뒤, mapper()함수로 둘을 연결해주면 클래스가 간결해지니까 코드가 훨씬 보기 수월해 지겠죠.

from sqlalchemy import Table, MetaData, Column, Integer, String, ForeignKey
from sqlalchemy.orm import mapper

metadata = MetaData()

user = Table('user', metadata,
            Column('id', Integer, primary_key=True),
            Column('name', String(50)),
            Column('fullname', String(50)),
            Column('nickname', String(12))
        )

class User(object):
    def __init__(self, name, fullname, nickname):
        self.name = name
        self.fullname = fullname
        self.nickname = nickname

# sqlalchemy.orm.mapper()는 현재 없어졌다네요
# 이 방법은 옛날 방법이니까 가급적 사용하지 마시고요
# 아래에 소개해 드리는 ORM으로 구현한 방법을 사용하시기 바랍니다.
mapper(User, user)

참고로 create_all()함수를 실행하면 위의 테이블을 생성 할수 있게 됩니다.

metadata.create_all(engin)

ORM의 DeclarativeBase

Base클래스를 만들어서 MetaData에 접근할수 있습니다.

from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
    pass

이렇게 하면 MetaData를 따로 가져오지 않아도 필요한 객체에 접근할 수 있습니다.

>>> Base.metadata
MetaData()
>>> Base.registry
<sqlalchemy.orm.decl_api.registry object at 0x1069cf0e0>

아래는 Declarative를 이용하여 두개의 모델을 매핑하는 PEP484에 따른 가장 현대적인 유형의 코드입니다.

from typing import List
from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(30))
    fullname: Mapped[Optional[str]]
    addresses: Mapped[List["Address"]] = relationship(back_populates="user")

    def __repr__(self) -> str:
        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

class Address(Base):
    __tablename__ = "address"

    id: Mapped[int] = mapped_column(primary_key=True)
    email_address: Mapped[str]
    user_id = mapped_column(ForeignKey("user_account.id"))
    user: Mapped[User] = relationship(back_populates="addresses")
    def __repr__(self) -> str:
        return f"Address(id={self.id!r}, email_address={self.email_address!r})"

주석때문에 너무 정신없죠. 주석빼고 이렇게 써도 됩니다.

from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy import ForeignKey
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "user_account"

    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(30), nullable=False)
    fullname = mapped_column(String(30))
    addresses = relationship("Address", back_populates="user")

    def __repr__(self) -> str:
        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

class Address(Base):
    __tablename__ = "address"

    id = mapped_column(Integer, primary_key=True)
    user_id = mapped_column(Integer, ForeignKey("user_account.id"))
    email_address = mapped_column(String(30), nullable=True)
    user = relationship("User", back_populates="addresses")

    def __repr__(self) -> str:
        return f"Address(id={self.id!r}, user_id={self.user_id!r}, email_address={self.email_address!r})"

마찬가지로 이렇게 선언한 후에 create_all()함수를 호출하면 테이블이 생성됩니다.

Base.metadata.create_all(engine)

그런데 이렇게 선언을 다 해놓고, 옛날 방법으로 데이타를 불러온다면 말짱 도로묵입니다. 아래와 같이 암만 User를 ORM Base로 선언하고, ORM의 mapped_column()를 이용하여 칼럼을 선언했어도. engine.connect()를 통해서 statement을 실행하면 그 결과는 ORM이 아니라 그냥 옛날에 사용하던 방법으로 나오던 쿼리 결과일 뿐입니다.

from sqlalchemy import select

stmt = select(User).where(User.name == "spongebob")
with engine.connect() as conn:
    for row in conn.execute(stmt):
        print(row)
# engine.connect.execute()을 통해서 실행한 결과 그냥 데이타 Set임
(1, 'spongebob', 'Spongebob Squarepants')

ORM을 제대로 사용하시려면, 선언도 ORM으로 하시고 실행도 아래와 같이 ORM.Session으로 하셔야합니다.

from sqlalchemy.orm import Session

with Session(engine) as session:
    row = session.execute(select(User)).first()
    print(row)
# ORM으로 실행한 결과 모델클래스의 객체가 반환됨
(User(id=1, name='spongebob', fullname='Spongebob Squarepants'),)

다음 시간에 이어서 데이타를 입력하고 수정하고 검색하는 내용을 배워보도록 하겠습니다. 오늘은 여기까지 잘 따라오셨습니다. 쉬는 시간에는 꼭 쉬세요^^

Resources

Flask에서 Template사용하기

매우 간단하게 설명하겠습니다. Flask를 아직 설치하지 않으신 분은 지금 설치하세요.

   pip install Flask

app.py

from flask import Flask, render_template

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

if __name__ == "__main__":
    app.run(debug=True)

그리고 templates폴더를 하나 만들어서 그 안에 index.html을 넣습니다.

<!DOCTYPE html>
<html>
<head>
    <title>My Flask App</title>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>

실행해볼게요

python app.py

How to Build a Flask REST API

Python과 venv가 기본적으로 설치되어 있는 환경에서 작업합니다.

필요한 패키지들은 아래 requirement.txt와 같습니다.

aniso8601==9.0.1
blinker==1.9.0
click==8.1.7
colorama==0.4.6
Flask==3.1.0
Flask-RESTful==0.3.10
Flask-SQLAlchemy==3.1.1
greenlet==3.1.1
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
pytz==2024.2
six==1.16.0
SQLAlchemy==2.0.36
typing_extensions==4.12.2
Werkzeug==3.1.3

프로젝트폴더에 api.py라는 이름으로 파일을 하나 만들어서 아래와 같이 저장합니다.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return '<h1>Flask REST API</h1>'

if __name__=="__main__":
    app.run(debug=True)

그리고 해당 앱을 실행함으로써 Flask API서버를 실행합니다.

 $ python api.py
 * Serving Flask app 'api'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 211-059-641

브라우저에서 http://127.0.0.1:5000를 열어서 서버가 문제없이 작동하는지 확인합니다.

이제 데이타베이스를 연동해볼게요. api.py에 SQLAlchemy를 사용하겠다고 명시해주세요. 그리고 데이타베이스 위치를 명시해준 뒤에 아래와 같이 User모델을 추가해주세요.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
db = SQLAlchemy(app)

class UserModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(80), unique=True, nullable=False)

    def __repr__(self):
        return f"User(name = {self.name}, email = {self.name})"

...

이 파일에서 정의한 모델을 데이타베이스에 생성할 파일을 하나 만듭니다. create_db.py를 만들어서 아래와 같이 코딩해주세요. api.py에서 app과 db를 가져와서 해당 모델들을 생성해줍니다.

from api import app, db 

with app.app_context():
    db.create_all()

그리고 실행합니다.

python create_db.py

그러면 instance 폴더에 SQLite데이타베이스가 생성된것을 확인하실수 있습니다.

$ ls -al instance
total 16
drwxr-xr-x   3 slim  staff     96 Nov 24 01:51 ./
drwxr-xr-x  17 slim  staff    544 Nov 24 01:49 ../
-rw-r--r--   1 slim  staff  16384 Nov 24 01:51 database.db

이제 User테이블에서 데이타를 가져오는 코드를 추가해볼게요. import 맨 끝에 flask_restful에서 Resource, Api, reqparse를 사용하겠다고 명시합니다. 그리고 GET, POST, DELETE등의 RestfulAPI에 필요한 프로토콜들을 사용하기 위해서 Api()함수를 실행하여 api변수에 담습니다. 그리고 UserModel밑에 Users라는 API클래스를 하나 선언합니다. 그리고 그 안에 get이라는 이름의 함수를 선언하는데요, 맞습니다. RestfulAPI에서 GET으로 해당 route이 호출되었을때 어떤 값을 넘겨줄지를 여기에서 정의하는 거에요. 저는 여기서 일단 UserModel에서 쿼리를 해오는데 일단 다 가져오도록 합니다. 그리고 그 값을 return하면 Flask가 척척 알아서 결과를 출력할거에요. 이제 이 클래스를 호출할 route을 그 밑에 추가합니다. 저는 /users/로 했어요.

...
from flask_restful import Resource, Api

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
db = SQLAlchemy(app)
api = Api(app)

class UserModel(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(80), unique=True, nullable=False)

    def __repr__(self):
        return f"User(name = {self.name}, email = {self.name})"

class Users(Resource):
    def get(self):
        users = UserModel.query.all()
        return users

api.add_resource(Users, '/users/')
...

그럼 서버를 다시 실행해볼까요?

python api.py

그리고 브라우저에서 http://127.0.0.1:5000/users/를 열어봅니다.

현재 User테이블에 아무것도 안들어가 있으니까 당연히 빈 배열방을 넘겨주겠죠. 그런데 RestAPI결과 값은 보통 JSON형식으로 보여줍니다. 결과를 보여주는 포멧을 한번 바꿔볼게요. JSON형식의 결과를 미리 정의하기 위해서는 flask_restful패키지의 fields가 필요합니다.

from flask_restful import Resource, Api, fields

그리고 UserModel뒤에 아래와 같이 입/출력 데이타의 JSON 포멧을 정의해줄게요.

userFields = {
    'id': fields.Integer,
    'name': fields.String,
    'email': fields.String,
}

그리고 나서 정의한 포멧을 GET함수에 추가해줄건데요. 이때 필요한 패키지가 또 있습니다. marshal_with라고 출력할 포멧을 지정할때 Flask에서 필요한 데코레이터입니다.

from flask_restful import Resource, Api, fields, marshal_with

데코레이터는요, 기존의 Object를 꾸미는 용도로 사용하는 건데요. 보통 Object위에 붙여서 기존의 Object형식은 그대로 사용하면서 해당 Object에 필요한 함수나 변수들을 추가로 넣을 수 있는 기능입니다. Users클래스의 get()함수 바로 위에 @으로 시작되는 코드를 아래와 같이 추가해주세요. marshal_with에게 userFields를 인자로 넘겨주어서, 출력형식으로는 아까 정의해둔 userFields를 사용하겠다고 명시합니다.

class Users(Resource):
    @marshal_with(userFields)
    def get(self):
        users = UserModel.query.all()
        return users

현재 우리가 가지고 있는 데이타가 없기때문에 사용자 정보를 추가하기 위해서는 POST로 데이타를 받아서 처리해주는 부분이 필요합니다. 사용자에게 데이타를 받기 위해서는 받을 인자들을 미리 정의해 놓아야하는데 이때 Flask에서 사용하는 패키지가 바로 reqparse입니다.

from flask_restful import Resource, Api, fields, marshal_with, reqparse

그리고 userFields를 선언하기 전에 아래와 같이 사용자인자를 미리 정의합니다. reqparse패키지의 RequestParser()함수를 사용하여 user_args객체를 선언하고 해당 객체에 add_argument()함수를 통해서 각 인자들의 세부사항을 적어줍니다. name이랑 email 이렇게 두개를 받을 건데요, type은 문자열, 그리고 필수입력사항으로 정의할게요. 이렇게 선언을 하면 HTTP Request가 들어왔을때 함께 들어온 이름, 여기서는 name과 email이 되겠죠. 해당 이름의 인자들을 Request데이타에서 받아서 친절하게 user_args에 저장을 해줍니다.

user_args = reqparse.RequestParser()
user_args.add_argument('name', type=str, required=True, help="Name cannot be blank")
user_args.add_argument('email', type=str, required=True, help="Email cannot be blank")

이제 Users클래스에 POST요청이 들어왔을때 처리해줄 post()함수를 선언합니다. 사용자가 입력할 데이타도 Body에서 JSON으로 전달받을거니까 여기서도 마찬가지로 @marshal_with(userFields)데코레이터를 사용해줍니다. 위에서 받아온 매개변수, user_args를 사용할건데요. reqparse.RequestParser()가 넘겨준 결과값은 Object이기때문에 편리한 사용을 위해서 해당 Object의 pase_args()함수를 호출해서 배열에 담아줄게요. 그리고 해당값으로 UserModel을 하나 생성하고, 그 모델을 db.session에 추가해줌으로써 Insert를 완료하게됩니다. 사용자를 추가한 후에는 사용자목록을 받아와서 보여주도록 할게요. 이때 반환하는 HTTP code는 사용자를 성공적으로 생성했다는 의미로 201을 넘겨줍니다.

    @marshal_with(userFields)
    def post(self):
        args = user_args.parse_args()
        user = UserModel(name=args["name"], email=args["email"])
        db.session.add(user)
        db.session.commit()
        users = UserModel.query.all()
        return users, 201

결과를 확인해보려면, Postman이나 Thunder Client같은 클라이언트 툴이 필요한데요, 저는 Visual Studio Code에서 Thunder Client모듈을 설치해서 테스트를 해보도록 할게요. 프로토콜은 POST로 경로를 적어주고, Body에 저장할 이름과 이메일을 JSON으로 넘겨줍니다.

전송버튼을 누르면 201코드와 함께 위에서 코딩한 대로 사용자목록을 Response로 반환해줍니다.

사용자를 생성할때 필수사항으로 정의한 필드를 빠뜨리면 어떻게 될까요? 이메일을 넣지않고 호출을 해볼게요.

Flask가 친절하게도 400코드를 메세지와 함께 반환해주네요.

이번에는 ID를 넘겨주어 한명의 User만 가져오는 기능을 넣어볼게요. Users클래스 밑에 User라는 리소스를 하나 더 만들거에요. 마찬가지로 결과를 JSON으로 결과를 보여줄거니까 marshal_with데코레이터 추가하고요, 프로토콜은 GET이 될테니까 get()함수에다 정의해줄게요. 이때 get()함수에 self이외에 id를 추가해서 사용자에게서 받은 id를 함수안에서 사용할 수 있게 같이 받습니다. 그리고 넘겨받은 id를 가지고 UserModel의 id에 넣어서 filtering을 합니다. 그럴일은 없겠지만 혹시 결과가 여러개 나오면 처음 나오는것만 하나 보여주도록합니다. 이때, 혹시 검색하는 사용자가 없다면 더이상 진행을 하지않고, abort함수를 사용해서 404를 반환하여 찾는 사람이 없다는 걸 알려줍니다. 리소스가 다 완성이 되면 api.add_resource()를 통해서 새로운 route를 추가하여 /users/경로에 정수형의 id가 붙어서 들어오면 User라는 리소스를 통해서 요청을 처리한다고 명시합니다.

from flask_restful import Resource, Api, fields, marshal_with, reqparse, abort

class User(Resource):
    @marshal_with(userFields)
    def get(self, id):
        user = UserModel.query.filter_by(id=id).first()
        if not user:
            abort(404, "User not found")
        return user

api.add_resource(Users, '/users/')
api.add_resource(User, '/users/<int:id>')

요청은 /users/id로 아래와 같이 하면 됩니다.

결과는 배열없이 단일 객체로 id가 1인 사용자를 반환합니다.

이번에는 기존의 데이타를 수정하는 경로를 PATCH프로토콜로 구현해볼게요. User클래스에 patch함수를 정의하고, JSON으로 데이타를 주고 받을 거니까 marshal_with 데코레이터를 추가합니다. Request URL에서 id를 받아오도록 함수의 인자에 명시하고, Body에 JSON으로 받아온 매개변수를 args에 배열로 저장합니다. id를 가지고 수정할 사용자를 찾아와서 user모델에 저장한뒤 해당 모델의 name과 email을 받아온 데이타로 대체합니다. 그리고나서 commit()를 하면 변경된 모델이 데이타베이스에 적용이됩니다.

    @marshal_with(userFields)
    def patch(self, id):
        args = user_args.parse_args()
        user = UserModel.query.filter_by(id=id).first()
        if not user:
            abort(404, "User not found")
        user.name = args["name"]
        user.email = args["email"]
        db.session.commit()
        return user

PATCH프로토콜을 테스트할때는 경로에 변경할 사용자의 ID를 명시하고 Body에 JSON포멧으로 코드에서 명시한 형식에 따라 수정할 데이타를 넘겨줍니다.

그러면 변경된 결과를 반환하죠.

마지막으로 DELETE프로토콜도 구현을 해볼게요. 마찬가지로 marshal_with데코레이터를 넣고, id를 넘겨받는 delete함수를 선언해서 해당 id로 사용자를 찾습니다. 그리고 db.session.delete()함수를 사용해서 user를 삭제한뒤 commit()하면 처리가 되는데 다 처리가 되면 사용자목록을 반환합니다.

    @marshal_with(userFields)
    def delete(self, id):
        user = UserModel.query.filter_by(id=id).first()
        if not user:
            abort(404, "User not found")
        db.session.delete(user)
        db.session.commit()
        users = UserModel.query.all()
        return users

요청은 Body없이 DELETE프로토콜에 id를 넣은 경로로 호출을 합니다. 삭제를 위해서 제가 미리 id가 2인 사용자를 등록해놨어요. 한번 지워볼게요.

그러면 삭제완료후 남은 사용자의 목록을 보여줍니다.

여기까지 잘 따라와 주셔서 감사합니다.

Resources

Image Resizing Script in Python

우선 패키지 설치하시고요

pip install Pillow
# 아니면 python -m pip install Pillow

아래 코드를 resize_image.py로 저장하세요

from PIL import Image
import os

file_extention = ".JPG"
input_dir = "./input"  # Replace with your actual input directory
output_dir = "./output"  # Replace with your desired output directory

def resize_image(input_dir, output_dir, new_width=800):
    """Resizes all JPG images in the input directory to the specified width and saves them in the output directory."""

    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    for filename in os.listdir(input_dir):
        if filename.endswith(file_extention):
            filepath = os.path.join(input_dir, filename)
            with Image.open(filepath) as img:
                width, height = img.size
                new_height = int(height * new_width / width)
                resized_img = img.resize((new_width, new_height), Image.LANCZOS)
                output_path = os.path.join(output_dir, filename)
                resized_img.save(output_path)
                print(f"Resized {filename} to {new_width}x{new_height}")

if __name__ == "__main__":
    resize_image(input_dir, output_dir)

input이라는 폴더 하나 만드시고, 거기다가 JPG이미지 넣으세요. 혹시 확장자가 jpg소문자이면 코드에서 file_extention값을 소문자로 바꿔주세요. 저는 함수 호출할때 새로 만들 이미지의 가로크기를 지정해주도록 했는데 기본값은 800이거든요. 혹시 다른 사이즈 넣고 싶으시면 함수 호출할때 같이 넣어주시면 되요. 이제 코드를 실행할게요.

python3 resize_image.py

그러면 결과를 출력하면서 이미지를 리사이징합니다.

Resized IMG_0809.JPG to 800x600
Resized IMG_0821.JPG to 800x600
...

실행이 끝나고 나면 output이라는 폴더가 생겼을거에요.

감사합니다.

PHP 맛보기

PHP가 실행이 되는 웹호스팅을 이미 가지고 있다는 가정하에 설명을 드릴게요.

아래 코드를 웹서버 루트폴더에 index.php라는 파일명으로 저장을 하세요.

<?php
echo "Hello World!";
?>

그런다음 https://도메인/하시면 화면에 Hello World!가 보이실거에요.

<?php ?>바깥에는 HTML코드를 자유롭게 사용하실 수 있습니다.

<!DOCTYPE html>
<html>
<body>
 
<?php
echo "Hello World!";
?>

</body>
</html>

Archived: PHP

venv & docker setting for Flask API

venv

여러 프로젝트를 하나의 장비에서 실행할때 각 프로젝트마다 사용하는 패키지들의 버젼이 달라서 충돌이 생길수 있는데 그런 일을 미연에 방지하기 위해서 각 프로젝트 마다 virtual environment, venv를 설정하고 충돌없이 사용할수 있도록합니다. 아래 명령어를 통해서 venv를 하나 생성해주세요.

$ python3 -m venv ./venv

그러면 venv폴더에 독립된 개발환경이 저장이 됩니다. 생성만 한다고 해서 바로 실행되는건 아니에요. 아래 명령어로 활성화를 시켜줍니다.

$ source ./venv/bin/activate

그러면 프롬프트 앞에 (venv)라고 뜰거에요. 이제 이 상태에서 개발 진행하시고 venv를 비활성화 시키고 싶으시면 deactivate명령어로 venv에서 빠져나오시면 됩니다.

(venv) $ deactivate

Flask

이제 해당 venv에 Flask를 설치해보도록 하겠습니다.

pip install Flask

버젼을 확인해볼까요?

$ Flask --version
Python 3.13.0
Flask 3.0.3
Werkzeug 3.1.3

Docker

우선 Flask App을 하나 만들어서 app.py로 저장합니다.

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, World!"

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000)

requirements.txt라는 파일을 만들어서 그안에 아래와 같이 저장합니다.

Flask==3.0.3

Dockerfile이라는 파일을 만들어서 아래와 같이 명령을 나열합니다.

FROM python:3-slim-buster

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["flask", "run", "--host=0.0.0.0"]

Command line에 아래의 명령어를 입력해서 Docker Image를 build합니다.

(venv)$ docker build -t cms .

Docker이미지가 잘 생성이 되었는지 확인해볼까요?

(venv)$ docker image ls
REPOSITORY                    TAG                            IMAGE ID       CREATED          SIZE
cms                           latest                         8fcfeb378ee4   12 minutes ago   151MB

만들어진 이미지를 한번 실행해 볼게요. 나중에 실행을 취소할때 편리하도록 컨테이너에 cms라는 이름을 주고 실행을 하도록 하겠습니다.

(venv)$ docker run --name cms -d -p 5000:5000 cms
7e672b644d6470c14ff5fed1b53e0420662941a1ee2b5218193ef1c1dd32b147

docker ps를 통해서 실행이 잘 되었는지 확인해보실 수 있습니다.

(venv)$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                    NAMES
0c873bfa10db   cms       "flask run --host=0.…"   4 seconds ago   Up 3 seconds   0.0.0.0:5000->5000/tcp   cms

브라우저를 열고 http://localhost:5000/ 를 입력해서 확인해볼게요

컨테이너를 실행중지하시려면 아까 부여한 이름으로 docker stop을 하시면 됩니다.

(venv)$ docker stop cms
cms
(venv)$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Install Docker Desktop on Mac

Docker 웹사이트에 들어가서 본인의 컴퓨터사양에 맞는 Docker를 다운받습니다. 저는 Intel칩을 사용하기 때문에 Docker Desktop for Mac with Intel chip을 클릭해서 다운 받았습니다.

브라우저 우상단의 Docker.dmg를 실행해서 Applications에 Docker를 복사해 줍니다.

하단메뉴의 Launchpad를 열어서 Docker를 실행해줍니다. 열겠냐고 물어보면 확인을 눌러줍니다.

실행을 하면 아래와 같은 데스크탑 화면이 보일거에요.

사용하는 방법에 대해서는 다음에 설명해 드리도록 하겠습니다.