KR_Pyramid - somaz94/python-study GitHub Wiki

Python Pyramid κ°œλ… 정리


1️⃣ Pyramid 기초

PyramidλŠ” μœ μ—°ν•˜κ³  ν™•μž₯ κ°€λŠ₯ν•œ 파이썬 μ›Ή ν”„λ ˆμž„μ›Œν¬μ΄λ‹€. ν™•μž₯μ„±κ³Ό λͺ¨λ“ˆμ„±μ— 쀑점을 λ‘” 이 ν”„λ ˆμž„μ›Œν¬λŠ” κ°„λ‹¨ν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜λΆ€ν„° λŒ€κ·œλͺ¨ μ—”ν„°ν”„λΌμ΄μ¦ˆ μˆ˜μ€€μ˜ μ›Ή μ„œλΉ„μŠ€κΉŒμ§€ λ‹€μ–‘ν•œ 규λͺ¨μ˜ ν”„λ‘œμ νŠΈμ— μ ν•©ν•˜λ‹€.

from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response

def hello_world(request):
    """λ‹¨μˆœν•œ Hello World 응닡을 λ°˜ν™˜ν•˜λŠ” λ·° ν•¨μˆ˜"""
    return Response('Hello World!')

def create_user(request):
    """μ‚¬μš©μž 생성 API μ—”λ“œν¬μΈνŠΈ"""
    user_data = request.json_body
    # μ‚¬μš©μž 생성 둜직
    return Response(json={'status': 'created', 'user_id': 123})

class PyramidApp:
    """Pyramid μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ„€μ •ν•˜κ³  μ‹€ν–‰ν•˜λŠ” 클래슀"""
    
    def __init__(self, host='0.0.0.0', port=6543):
        self.host = host
        self.port = port
        self.config = None
        self.app = None
    
    def setup(self):
        """μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„€μ •"""
        self.config = Configurator()
        
        # κΈ°λ³Έ 라우트 μ„€μ •
        self.config.add_route('hello', '/')
        self.config.add_view(hello_world, route_name='hello')
        
        # API 라우트 μ„€μ •
        self.config.add_route('users', '/users')
        self.config.add_view(create_user, route_name='users', 
                           request_method='POST', renderer='json')
        
        # 정적 파일 μ„€μ •
        self.config.add_static_view('static', 'static', cache_max_age=3600)
        
        # WSGI μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 생성
        self.app = self.config.make_wsgi_app()
        return self
    
    def serve(self):
        """μ„œλ²„ μ‹€ν–‰"""
        print(f"μ„œλ²„ μ‹œμž‘: http://{self.host}:{self.port}")
        server = make_server(self.host, self.port, self.app)
        server.serve_forever()

if __name__ == '__main__':
    app = PyramidApp()
    app.setup().serve()

βœ… νŠΉμ§•:

  • μœ μ—°ν•œ λΌμš°νŒ… μ„€μ •
  • λ‹€μ–‘ν•œ λ·° ν•¨μˆ˜ μ •μ˜ 방식
  • WSGI ν˜Έν™˜ μ„œλ²„ μ‹€ν–‰
  • ν™•μž₯성이 λ›°μ–΄λ‚œ ꡬ쑰
  • μ„€μ • κ°€λŠ₯ν•œ 미듀웨어
  • λ‹€μ–‘ν•œ ν…œν”Œλ¦Ώ μ—”μ§„ 지원
  • ν”ŒλŸ¬κ·ΈμΈ 기반 μ•„ν‚€ν…μ²˜


2️⃣ λΌμš°νŒ…κ³Ό λ·°

PyramidλŠ” μœ μ—°ν•œ λΌμš°νŒ… μ‹œμŠ€ν…œκ³Ό λ‹€μ–‘ν•œ λ·° μ •μ˜ 방식을 μ œκ³΅ν•˜μ—¬ μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ URL ꡬ쑰와 μš”μ²­ 처리λ₯Ό 효과적으둜 관리할 수 μžˆλ‹€.

from pyramid.view import view_config, view_defaults
from pyramid.response import Response
from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest

# 클래슀 기반 λ·° μ •μ˜
@view_defaults(route_name='users', renderer='json')
class UserViews:
    """μ‚¬μš©μž 관리λ₯Ό μœ„ν•œ λ·° 클래슀"""
    
    def __init__(self, request):
        self.request = request
        self.user_db = {
            '1': {'id': '1', 'name': 'User 1', 'email': '[email protected]'},
            '2': {'id': '2', 'name': 'User 2', 'email': '[email protected]'}
        }
    
    @view_config(request_method='GET')
    def get_users(self):
        """λͺ¨λ“  μ‚¬μš©μž 쑰회"""
        return {'users': list(self.user_db.values())}
    
    @view_config(request_method='POST')
    def create_user(self):
        """μƒˆ μ‚¬μš©μž 생성"""
        try:
            data = self.request.json_body
            if not all(k in data for k in ('name', 'email')):
                return HTTPBadRequest(json={'error': 'ν•„μˆ˜ ν•„λ“œκ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€'})
            
            # κ°„λ‹¨ν•œ ID 생성 (μ‹€μ œλ‘œλŠ” UUID λ“± μ‚¬μš©)
            new_id = str(len(self.user_db) + 1)
            self.user_db[new_id] = {
                'id': new_id,
                'name': data['name'],
                'email': data['email']
            }
            return {'status': 'created', 'user': self.user_db[new_id]}
        except Exception as e:
            return HTTPBadRequest(json={'error': str(e)})

# ν•¨μˆ˜ 기반 λ·° (νŒ¨ν„΄ λ§€μΉ­ μ‚¬μš©)
@view_config(route_name='user', request_method='GET', renderer='json')
def get_user(request):
    """νŠΉμ • μ‚¬μš©μž 정보 쑰회"""
    user_id = request.matchdict['id']
    user_db = {
        '1': {'id': '1', 'name': 'User 1', 'email': '[email protected]'},
        '2': {'id': '2', 'name': 'User 2', 'email': '[email protected]'}
    }
    
    if user_id not in user_db:
        raise HTTPNotFound(json={'error': f'User {user_id} not found'})
    
    return {'user': user_db[user_id]}

# 라우트 섀정을 μœ„ν•œ ν•¨μˆ˜
def includeme(config):
    """라우트 섀정을 μ€‘μ•™ν™”ν•˜λŠ” ν•¨μˆ˜"""
    # 기본 라우트
    config.add_route('home', '/')
    config.add_view(lambda r: Response('Welcome to Pyramid!'), route_name='home')
    
    # RESTful API 라우트
    config.add_route('users', '/api/users')
    config.add_route('user', '/api/users/{id}')
    
    # μŠ€μΊ”ν•˜μ—¬ λ°μ½”λ ˆμ΄ν„°λ‘œ μ •μ˜λœ λ·° 등둝
    config.scan('.')

βœ… νŠΉμ§•:

  • 클래슀 기반 λ·° 지원
  • ν•¨μˆ˜ 기반 λ·° 지원
  • URL νŒ¨ν„΄ λ§€μΉ­ (경둜 λ³€μˆ˜)
  • λ°μ½”λ ˆμ΄ν„°λ₯Ό ν†΅ν•œ λ·° μ„€μ •
  • λ‹€μ–‘ν•œ HTTP λ©”μ„œλ“œ 처리
  • JSON 및 λ‹€μ–‘ν•œ λ Œλ”λŸ¬ 지원
  • HTTP μ˜ˆμ™Έ 처리 λ©”μ»€λ‹ˆμ¦˜
  • 쀑첩 λΌμš°νŒ… ꡬ쑰 지원


3️⃣ λ°μ΄ν„°λ² μ΄μŠ€ 톡합

PyramidλŠ” SQLAlchemy와 같은 ORM λΌμ΄λΈŒλŸ¬λ¦¬μ™€ μ‰½κ²Œ ν†΅ν•©λ˜μ–΄ λ°μ΄ν„°λ² μ΄μŠ€ μž‘μ—…μ„ 효율적으둜 μ²˜λ¦¬ν•  수 μžˆλ‹€.

from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, scoped_session
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.view import view_config
from datetime import datetime
import transaction

# SQLAlchemy λͺ¨λΈ μ •μ˜
Base = declarative_base()

class User(Base):
    """μ‚¬μš©μž λͺ¨λΈ"""
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    posts = relationship('Post', back_populates='author', cascade='all, delete-orphan')
    
    def __repr__(self):
        return f"<User(username='{self.username}', email='{self.email}')>"
    
    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'created_at': self.created_at.isoformat(),
            'post_count': len(self.posts)
        }

class Post(Base):
    """κ²Œμ‹œλ¬Ό λͺ¨λΈ"""
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(100), nullable=False)
    content = Column(String, nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    
    author = relationship('User', back_populates='posts')
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content,
            'created_at': self.created_at.isoformat(),
            'user_id': self.user_id,
            'author_username': self.author.username
        }

# λ°μ΄ν„°λ² μ΄μŠ€ μ„Έμ…˜ 관리
def get_db_session(request):
    """μš”μ²­ 객체에 λ°μ΄ν„°λ² μ΄μŠ€ μ„Έμ…˜μ„ μΆ”κ°€ν•˜λŠ” νŒ©ν† λ¦¬ ν•¨μˆ˜"""
    session = request.registry.dbmaker()
    
    def cleanup(request):
        if request.exception is not None:
            transaction.abort()
            session.rollback()
        else:
            try:
                transaction.commit()
            except:
                transaction.abort()
                session.rollback()
                raise
        session.close()
    
    request.add_finished_callback(cleanup)
    return session

# λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” ν•¨μˆ˜
def initialize_db(settings):
    """λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” 및 μ„Έμ…˜ νŒ©ν† λ¦¬ 생성"""
    engine = create_engine(settings['sqlalchemy.url'])
    Base.metadata.create_all(engine)
    
    session_factory = sessionmaker(bind=engine)
    return scoped_session(session_factory)

# λ·° ν•¨μˆ˜ 예제
@view_config(route_name='users', request_method='POST', renderer='json')
def create_user(request):
    """μƒˆ μ‚¬μš©μž 생성 API"""
    session = request.db
    
    try:
        user_data = request.json_body
        
        # μ‚¬μš©μž 생성
        user = User(
            username=user_data['username'],
            email=user_data['email']
        )
        
        session.add(user)
        session.flush()  # ID 생성을 μœ„ν•΄ ν”ŒλŸ¬μ‹œ
        
        return user.to_dict()
    except Exception as e:
        session.rollback()
        return {'error': str(e)}

@view_config(route_name='user_posts', request_method='POST', renderer='json')
def create_post(request):
    """μ‚¬μš©μžμ˜ μƒˆ κ²Œμ‹œλ¬Ό 생성 API"""
    session = request.db
    user_id = int(request.matchdict['user_id'])
    
    try:
        # μ‚¬μš©μž 쑴재 μ—¬λΆ€ 확인
        user = session.query(User).filter_by(id=user_id).first()
        if not user:
            return {'error': 'User not found'}
        
        post_data = request.json_body
        post = Post(
            title=post_data['title'],
            content=post_data['content'],
            user_id=user_id
        )
        
        session.add(post)
        session.flush()
        
        return post.to_dict()
    except Exception as e:
        session.rollback()
        return {'error': str(e)}

# Pyramid μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ„€μ •
def main(global_config, **settings):
    """WSGI μ• ν”Œλ¦¬μΌ€μ΄μ…˜ νŒ©ν† λ¦¬"""
    config = Configurator(settings=settings)
    
    # λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”
    dbmaker = initialize_db(settings)
    config.registry.dbmaker = dbmaker
    config.add_request_method(get_db_session, 'db', reify=True)
    
    # 라우트 μ„€μ •
    config.add_route('users', '/api/users')
    config.add_route('user', '/api/users/{id}')
    config.add_route('user_posts', '/api/users/{user_id}/posts')
    
    config.scan()
    return config.make_wsgi_app()

βœ… νŠΉμ§•:

  • SQLAlchemy ORM 톡합
  • νŠΈλžœμž­μ…˜ 관리
  • μ„Έμ…˜ 라이프사이클 관리
  • λͺ¨λΈ μ •μ˜ 및 관계 μ„€μ •
  • μš”μ²­λ³„ λ°μ΄ν„°λ² μ΄μŠ€ μ„Έμ…˜
  • μ˜ˆμ™Έ μ²˜λ¦¬μ™€ λ‘€λ°± λ©”μ»€λ‹ˆμ¦˜
  • JSON 응닡 직렬화
  • λ‹€μ–‘ν•œ λ°μ΄ν„°λ² μ΄μŠ€ 지원


4️⃣ 인증과 κΆŒν•œ

from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import (
    Allow,
    Everyone,
    Authenticated,
    ALL_PERMISSIONS
)

class RootFactory(object):
    __acl__ = [
        (Allow, Everyone, 'view'),
        (Allow, Authenticated, 'create'),
        (Allow, 'admin', ALL_PERMISSIONS)
    ]
    
    def __init__(self, request):
        pass

def groupfinder(userid, request):
    user = request.db.query(User).filter_by(id=userid).first()
    if user:
        return ['group:' + g.name for g in user.groups]

@view_config(route_name='secure', permission='view')
def secure_view(request):
    return Response('This is a secure view')

βœ… νŠΉμ§•:

  • 인증 μ •μ±…
  • κΆŒν•œ 관리
  • ACL μ„€μ •
  • μœ μ—°ν•œ λ³΄μ•ˆ λͺ¨λΈ
  • μ—­ν•  기반 μ ‘κ·Ό μ œμ–΄
  • μ„Έμ…˜ 관리 μ˜΅μ…˜
  • λ³΄μ•ˆ μ •μ±… 톡합


5️⃣ ν…œν”Œλ¦Ώ 및 ν”„λ‘ νŠΈμ—”λ“œ 톡합

PyramidλŠ” λ‹€μ–‘ν•œ ν…œν”Œλ¦Ώ 엔진을 μ§€μ›ν•˜κ³  정적 파일 제곡 κΈ°λŠ₯을 톡해 ν”„λ‘ νŠΈμ—”λ“œ ν”„λ ˆμž„μ›Œν¬μ™€ μ‰½κ²Œ ν†΅ν•©λœλ‹€.

from pyramid.view import view_config
from pyramid.renderers import render_to_response
import json

# Jinja2 ν…œν”Œλ¦Ώ μ‚¬μš© 예제
@view_config(route_name='home', renderer='templates/home.jinja2')
def home_view(request):
    """Jinja2 ν…œν”Œλ¦Ώμ„ μ‚¬μš©ν•˜λŠ” ν™ˆ νŽ˜μ΄μ§€ λ·°"""
    return {
        'title': 'Pyramid μ• ν”Œλ¦¬μΌ€μ΄μ…˜',
        'message': 'ν™˜μ˜ν•©λ‹ˆλ‹€!',
        'features': [
            'μœ μ—°ν•œ λΌμš°νŒ…',
            'κ°•λ ₯ν•œ λ³΄μ•ˆ λͺ¨λΈ',
            'λ°μ΄ν„°λ² μ΄μŠ€ 톡합',
            'λ‹€μ–‘ν•œ ν…œν”Œλ¦Ώ 지원'
        ]
    }

# Chameleon ν…œν”Œλ¦Ώ μ‚¬μš© 예제
@view_config(route_name='dashboard', renderer='templates/dashboard.pt')
def dashboard_view(request):
    """Chameleon ν…œν”Œλ¦Ώμ„ μ‚¬μš©ν•˜λŠ” λŒ€μ‹œλ³΄λ“œ λ·°"""
    # λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ 톡계 데이터 λ‘œλ“œ (μ˜ˆμ‹œ)
    stats = {
        'users': 1250,
        'posts': 5432,
        'comments': 18345,
        'active_users': 328
    }
    
    # 졜근 ν™œλ™ 데이터 (μ˜ˆμ‹œ)
    recent_activities = [
        {'user': 'user1', 'action': 'κ²Œμ‹œλ¬Ό μž‘μ„±', 'time': '5λΆ„ μ „'},
        {'user': 'user2', 'action': 'λŒ“κΈ€ μž‘μ„±', 'time': '10λΆ„ μ „'},
        {'user': 'user3', 'action': '둜그인', 'time': '15λΆ„ μ „'}
    ]
    
    return {
        'title': 'κ΄€λ¦¬μž λŒ€μ‹œλ³΄λ“œ',
        'stats': stats,
        'activities': recent_activities
    }

# API와 ν”„λ‘ νŠΈμ—”λ“œ 톡합
@view_config(route_name='app', renderer='templates/app.jinja2')
def spa_view(request):
    """SPA(Single Page Application) ν”„λ‘ νŠΈμ—”λ“œλ₯Ό μœ„ν•œ μ§„μž…μ """
    # 초기 μƒνƒœ 데이터 μ€€λΉ„
    initial_state = {
        'user': request.user.to_dict() if hasattr(request, 'user') else None,
        'config': {
            'apiUrl': request.route_url('api_root'),
            'staticUrl': request.static_url('myapp:static/'),
            'debug': request.registry.settings.get('debug', False)
        }
    }
    
    return {
        'title': 'Pyramid SPA',
        'initial_state': json.dumps(initial_state)
    }

# 정적 파일 μ„€μ •
def configure_static(config):
    """정적 파일 μ„€μ •"""
    # κΈ°λ³Έ 정적 파일 경둜
    config.add_static_view('static', 'static', cache_max_age=3600)
    
    # λΉŒλ“œλœ ν”„λ‘ νŠΈμ—”λ“œ μ• μ…‹ (예: React, Vue λ“±)
    config.add_static_view('assets', 'assets', cache_max_age=3600)
    
    # νŒŒλΉ„μ½˜ μ„€μ •
    config.add_route('favicon', '/favicon.ico')
    config.add_view(
        lambda r: FileResponse('static/favicon.ico', request=r),
        route_name='favicon'
    )

# ν…œν”Œλ¦Ώ μ„€μ •
def configure_templates(config):
    """ν…œν”Œλ¦Ώ μ—”μ§„ μ„€μ •"""
    # Jinja2 μ„€μ •
    config.include('pyramid_jinja2')
    config.add_jinja2_renderer('.html')
    config.add_jinja2_search_path('templates', name='.html')
    
    # Chameleon μ„€μ •
    config.include('pyramid_chameleon')
    
    # 곡톡 ν…œν”Œλ¦Ώ λ³€μˆ˜
    config.add_renderer_globals_subscriber(add_renderer_globals)

def add_renderer_globals(event):
    """λͺ¨λ“  ν…œν”Œλ¦Ώμ— μ „μ—­ λ³€μˆ˜ μΆ”κ°€"""
    request = event.get('request')
    if request is None:
        return
    
    event['site_name'] = 'Pyramid μ• ν”Œλ¦¬μΌ€μ΄μ…˜'
    event['current_year'] = datetime.now().year
    event['debug'] = request.registry.settings.get('debug', False)
    
    # 인증된 μ‚¬μš©μž 정보
    if hasattr(request, 'user'):
        event['user'] = request.user

βœ… νŠΉμ§•:

  • λ‹€μ–‘ν•œ ν…œν”Œλ¦Ώ μ—”μ§„ 지원 (Jinja2, Chameleon λ“±)
  • 정적 파일 제곡 및 캐싱
  • SPA ν”„λ ˆμž„μ›Œν¬μ™€ 톡합 (React, Vue, Angular λ“±)
  • κ΅­μ œν™” 및 μ§€μ—­ν™” 지원
  • μžμ‚° λ²ˆλ“€λ§ 톡합
  • ν…œν”Œλ¦Ώ ν™•μž₯ 및 상속
  • ν…œν”Œλ¦Ώ 헬퍼 ν•¨μˆ˜
  • μ „μ—­ ν…œν”Œλ¦Ώ λ³€μˆ˜


μ£Όμš” 팁

βœ… λͺ¨λ²” 사둀:

  • νŒ¨ν‚€μ§€ ꡬ쑰화: ν™•μž₯성을 μœ„ν•œ λͺ¨λ“ˆμ‹ 섀계 적용
  • λΌμš°νŒ… μ΅œμ ν™”: 체계적인 URL νŒ¨ν„΄ μ„€κ³„λ‘œ μœ μ§€λ³΄μˆ˜μ„± ν–₯상
  • λ³΄μ•ˆ κ°•ν™”: CSRF 보호, XSS λ°©μ§€, μ•ˆμ „ν•œ 인증 κ΅¬ν˜„
  • λ°μ΄ν„°λ² μ΄μŠ€ 섀계: κ΄€κ³„ν˜• λͺ¨λΈ 섀계와 λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ „λž΅ 수립
  • 캐싱 μ „λž΅: μ„œλ²„ λΆ€ν•˜ κ°μ†Œμ™€ 응닡 μ‹œκ°„ κ°œμ„ μ„ μœ„ν•œ 캐싱 적용
  • λ‘œκΉ… κ΅¬ν˜„: κ΅¬μ‘°ν™”λœ λ‘œκΉ…μœΌλ‘œ 문제 진단 및 λͺ¨λ‹ˆν„°λ§ μš©μ΄ν™”
  • ν…ŒμŠ€νŠΈ μžλ™ν™”: λ‹¨μœ„ ν…ŒμŠ€νŠΈ, 톡합 ν…ŒμŠ€νŠΈ, κΈ°λŠ₯ ν…ŒμŠ€νŠΈ μž‘μ„±
  • 배포 μžλ™ν™”: CI/CD νŒŒμ΄ν”„λΌμΈ ꡬ좕 및 μ»¨ν…Œμ΄λ„ˆν™” 적용
  • ν™•μž₯μ„± 섀계: ν”ŒλŸ¬κ·ΈμΈ μ•„ν‚€ν…μ²˜ ν™œμš© 및 이벀트 μ‹œμŠ€ν…œ λ„μž…
  • μ„±λŠ₯ μ΅œμ ν™”: ν”„λ‘œνŒŒμΌλ§ 및 병λͺ© ν˜„μƒ 식별/ν•΄κ²°
  • λ¬Έμ„œν™”: API λ¬Έμ„œμ™€ 개발자 κ°€μ΄λ“œ μž‘μ„±


⚠️ **GitHub.com Fallback** ⚠️