rscc-common
RSCC 공통 Python 라이브러리. 응답 봉투, 에러 코드, X-Trace-Id 전파, ASGI 미들웨어, 회복탄력성(재시도·서킷 브레이커·격벽·TTL 캐시), 보안(JWT·AES-GCM·웹훅 서명·로그 리댁션), 한국 도메인 유틸을 제공한다.
- 기본 설치는 의존성 0 (표준 라이브러리만). FastAPI/pydantic 통합과 암호화는 extra 로 opt-in.
- Java(
com.rscc:rscc-common-*)·JS(@rscc/common-*) 구현과 와이어 계약(응답 봉투·에러 코드· 헤더 규약 등)과 공유 테스트 벡터를 맞춘 폴리글랏 라이브러리의 Python 구현이다. 계약의 핵심 규칙은 각 모듈 docstring 에 요약돼 있어 이 패키지만으로 사용할 수 있다. - Python >= 3.11 (상한 없음), 타입 힌트 포함(
py.typed), 라이선스 MIT. - 시간 단위는 초 (Java/JS 는 ms —
초 × 1000 == ms).
설치
pip install rscc-common # 기본 — 의존성 0
pip install "rscc-common[fastapi]" # + fastapi>=0.110, pydantic>=2, httpx>=0.27
pip install "rscc-common[crypto]" # + cryptography>=42 (AES-256/GCM)
pip install "rscc-common[fastapi,crypto]" # 둘 다
| extra | 설치되는 것 | 이 extra 가 있어야 임포트되는 모듈 |
|---|---|---|
| (없음) | — | 아래 표에서 * 표시가 없는 모든 모듈 |
fastapi |
fastapi>=0.110, pydantic>=2 (v2 전용 API 사용), httpx>=0.27 | response, handlers, health_fastapi |
crypto |
cryptography>=42 (네이티브 휠) | crypto |
extra 없이 해당 모듈을 임포트하면 그 모듈만 ImportError/ModuleNotFoundError 이고,
패키지 자체(import rscc_common)와 나머지 모듈은 계속 동작한다. 순수 ASGI 미들웨어
(install_exception_handlers 를 제외한 install_*)와 httpx 훅은 fastapi/httpx 를 임포트하지
않으므로 기본 설치로도 임포트된다.
PyPI 대신 git 태그로 직설치할 수도 있다 (X.Y.Z 는 설치할 릴리스 버전):
pip install "rscc-common[fastapi] @ git+https://github.com/Jeonghyeon-Ryu/r-common.git@py-vX.Y.Z#subdirectory=python/rscc-common"
모듈 개요
* = extra 필요. 무의존 모듈은 패키지 최상위에서 서브모듈 네임스페이스로도 쓸 수 있고
(from rscc_common import jwt), 자주 쓰는 심볼은 최상위에 플랫 재노출된다
(configure_logging, TraceIdFilter, install_* 계열, get_trace_id/set_trace_id/
reset_trace_id/new_trace_id, trace_request_hook, UserContext/get_user_context,
ResultCode, CommonException 등).
웹·와이어 계약 (ASGI / FastAPI)
| 모듈 | 주요 API |
|---|---|
trace |
X-Trace-Id 규약 — install_trace_middleware(app)(수신 값 [A-Za-z0-9._-]{1,64} 이면 재사용, 아니면 8자리 hex 생성 → contextvar 바인딩 → 응답 에코), get_trace_id/set_trace_id/reset_trace_id/new_trace_id, httpx 발신 훅 trace_request_hook/async_trace_request_hook |
user_context |
내부 X-User-* 헤더 → UserContext contextvar — install_user_context_middleware(app), get_user_context(), httpx 훅 user_context_request_hook |
errors |
ResultCode enum(.code 문자열 / .http_status / .message), 비즈니스 예외 CommonException(result_code, custom_message=None) |
response * |
CommonResponse 봉투(success/code/message/data) — ok(data)/fail(result_code, message=None, data=None), to_wire()(data 없으면 키 생략), WireDateTime, BulkResultModel |
handlers * |
install_exception_handlers(app, *, validation_to_400=True) — CommonException·검증 오류(기본 400)·HTTPException·catch-all 500 을 실패 봉투로 변환 |
validation |
flatten_loc(loc) — pydantic loc → 필드 경로(items[0].name) |
access_log |
install_access_log(app, *, headers=(), include_query_string=False, slow_threshold_ms=None) — 요청당 1라인, 민감 헤더 마스킹 |
security_headers |
install_security_headers(app, *, hsts=False) — 보안 응답 헤더 프리셋(앱이 실은 헤더 우선) |
rate_limit |
install_rate_limit(app, *, capacity=20, refill_per_second=10.0) — IP 키별 429 게이트, TokenBucket |
client_ip |
resolve_client_ip/resolve_from_scope(scope, trusted_hops=…/trusted_proxies=…) — 안전 기본값 0홉(전달 헤더 무시) |
idempotency |
install_idempotency(app, store=None, …) — Idempotency-Key 중복 시 409, IdempotencyStore Protocol / InMemoryIdempotencyStore |
bulk |
부분 실패 봉투 — BulkResultBuilder, BulkResult/BulkItem, to_wire/from_wire |
query |
parse_sort(허용 목록 밖 → 400) / parse_page(관용 클램프) |
upload |
validate_upload(filename, size, head, *, max_size_bytes, allowed_extensions) — 크기→확장자→매직바이트 3중 검증, sniff |
health / health_fastapi * |
HealthRegistry, CheckResult / create_health_router(registry) — liveness·readiness(DOWN → 503) |
sse |
SSE 프레임 빌더 — frame_delta/frame_sources/frame_error/frame_done 등 |
datetimes |
오프셋 없는 와이어 datetime — to_wire_datetime/parse_wire_datetime(Z·오프셋은 변환 없이 버림)/strip_zone |
feature_flags |
FeatureFlags(source) + is_enabled(key) — 참 집합 {"true","1","on","yes"}, RSCC_FLAG_* 환경변수 폴백 |
metrics |
표준 메트릭·라벨 이름 문자열 상수 (계측 바인딩 없음) |
회복탄력성
| 모듈 | 주요 API |
|---|---|
retry |
retry_call(fn, *, retries=3, should_retry=…, override_delay=…, max_elapsed=…) / retry_call_async — 지수 백오프 + full jitter, parse_retry_after, is_retryable_status |
circuit_breaker |
CircuitBreaker(failure_threshold=, open_duration=) — call(fn, *args, **kwargs) / await call_async(fn, …) 권장(게이트 + 보고 1회 보장, 차단 시 CircuitOpenError), 수동 allow_request/on_success/on_failure/on_ignore, CircuitState |
bulkhead |
Bulkhead(max_concurrent=, max_queue=0)(with) / AsyncBulkhead(async with) — 만석 즉시 BulkheadFullError |
cache |
TtlCache(ttl=, max_entries=) / AsyncTtlCache — get(key, loader) single-flight(동시 호출은 로더 1회 공유, 실패·None 비캐시) |
rate_limit |
TokenBucket(capacity=, refill_per_second=) — try_acquire(n=1) 부족 시 대기 없이 False |
로깅·보안
| 모듈 | 주요 API |
|---|---|
logging |
configure_logging(log_dir, level, app_name, *, include_trace_id=True, clear_root_handlers=False, per_pid_file=False, quiet_uvicorn=False, noisy_loggers=()) — 콘솔 + 자정 롤링 파일(gzip, 30일), 로그 라인에 traceId. TraceIdFilter — 자체 로깅 구성용 |
log_redact |
install_secret_redaction() / redact_secrets(s) — Bearer/JWT/API 키/주민번호/카드번호 → [REDACTED:<type>] |
log_sanitize |
sanitize_log_value(value, max_length=…) — 제어 문자 치환 + 절단(로그 인젝션 방지) |
jwt |
HS256 — generate_token(user_id, secret, role=None, *, kid=None), decode_token(token, secret=None, *, keys=None)(kid 로테이션), extract_subject/extract_role, JwtError |
crypto * |
encrypt(plain, key) / decrypt(cipher, key) — AES-256/GCM, Base64(IV‖암호문‖태그), 실패 시 CryptoError |
webhook |
sign(secret, payload) → t=…,v1=… / verify(secrets, header, payload) — HMAC-SHA256, 다중 시크릿 로테이션, 스큐 300초 |
api_key |
issue("live")(또는 "test"), is_well_formed, checksum_ok, sha256_hex — rscc_live_… 48자 키 |
constant_time |
compare_digest — 시크릿·해시 상수시간 비교 |
masking |
mask_name/mask_phone/mask_email/mask_card_number/mask_secret |
한국 도메인 유틸
| 모듈 | 주요 API |
|---|---|
rrn |
is_valid_rrn(형식·성별코드·생년월일, 체크섬 미포함), is_foreigner_rrn, birth_date_of, checksum_ok_legacy |
bizno |
is_valid_business_number(사업자 10자리) / is_valid_corporate_number(법인 13자리) |
phone |
normalize_phone, classify_phone(mobile/landline/…), format_phone, to_e164 |
age |
age_man(만 나이) / age_by_year(연 나이) / age_insurance(보험 나이) |
money |
to_korean_words, to_formal_notation, abbreviate_amount(예: 1.2억) |
business_days |
BusinessDays(holidays=…) — 영업일 가감·판정(공휴일 주입형, 내장 테이블 없음) |
text.chosung / text.jamo / text.josa |
초성 변환 of·is_chosung_query / 자모 분해·결합·혼합 질의 matches / 조사 선택 pick·attach |
빠른 시작
import logging
import httpx
from fastapi import FastAPI
from rscc_common import configure_logging, install_trace_middleware, trace_request_hook
from rscc_common.errors import CommonException, ResultCode
from rscc_common.handlers import install_exception_handlers # [fastapi]
from rscc_common.response import CommonResponse # [fastapi]
configure_logging(log_dir="logs", level=logging.INFO, app_name="order-api", quiet_uvicorn=True)
# → [2026-09-26 12:00:00] INFO [3f9a1c2e] order_api - 주문 조회: 7
# (traceId 는 요청 컨텍스트 밖이면 "-". uvicorn --workers > 1 이면 per_pid_file=True 필수)
app = FastAPI()
install_trace_middleware(app) # X-Trace-Id 수신 재사용/생성 → contextvar → 응답 에코
install_exception_handlers(app) # 예외 → 실패 봉투 (CommonException·검증 400·HTTPException·500)
client = httpx.Client(event_hooks={"request": [trace_request_hook]}) # 다운스트림 전파
log = logging.getLogger("order_api")
@app.get("/orders/{order_id}")
def get_order(order_id: int):
log.info("주문 조회: %s", order_id)
if order_id > 1000:
raise CommonException(ResultCode.NOT_FOUND) # → 404 + {"success": false, "code": "404", ...}
return CommonResponse.ok({"id": order_id}).to_wire() # 반드시 to_wire() 반환
회복탄력성 — 재시도 안에서 서킷 브레이커로 게이트하고, OPEN 차단은 재시도하지 않는다:
from rscc_common.circuit_breaker import CircuitBreaker, CircuitOpenError
from rscc_common.retry import retry_call
cb = CircuitBreaker(failure_threshold=5, open_duration=30.0)
data = retry_call(
lambda attempt: cb.call(fetch_inventory, item_id), # 비동기는 await cb.call_async(afetch, item_id)
should_retry=lambda exc, attempt: not isinstance(exc, CircuitOpenError),
)
call/call_async 는 정상 반환 → 성공, Exception → 실패(같은 예외 재전파), 취소·인터럽트 등
그 밖의 BaseException → 판정 없는 반납(on_ignore)으로 정확히 1회 보고한다.
요청 밖(백그라운드 태스크 등)에서 traceId 잇기:
from rscc_common import new_trace_id, reset_trace_id, set_trace_id
token = set_trace_id(captured_trace_id or new_trace_id())
try:
do_background_work() # 이 안의 로그 라인·httpx 발신에 traceId 가 실린다
finally:
reset_trace_id(token)
자체 로깅 구성(dictConfig 등)을 쓰는 앱은 configure_logging 대신 TraceIdFilter 를
핸들러에 붙이고 포맷에 %(trace_id)s 를 넣는다:
from rscc_common.logging import TraceIdFilter
handler = logging.StreamHandler()
handler.addFilter(TraceIdFilter()) # dictConfig: {"()": "rscc_common.logging.TraceIdFilter"}
handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)-5s [%(trace_id)s] %(name)s - %(message)s"))
알려진 제약
- 미처리 예외의 500 응답과 X-Trace-Id — 트레이스 미들웨어는 Starlette
ServerErrorMiddleware안쪽에서 실행되므로 500 응답에 에코하지 못한다.install_exception_handlers의 catch-all 이 수신X-Trace-Id가 유효하면 그 값을 500 응답 헤더에 직접 에코해 부분 보완한다. 수신 값이 없어 미들웨어가 새로 생성한 경우엔 500 응답에 헤더가 없고, catch-all 의 에러 로그 라인도 traceId 가-로 찍힌다(그 시점엔 contextvar 가 이미 정리됨).HTTPException·검증 오류 등 핸들러 처리 경로(4xx)는 정상 에코된다. CommonResponse모델을 FastAPI 핸들러에서 직접 반환하지 말고.to_wire()를 반환할 것 — 직접 반환하면data: null이 실려 "데이터 없으면 키 생략" 계약이 깨진다.configure_logging- 기본 파일명(
{app_name}.log)은 프로세스 1개 전제 — uvicorn--workers> 1 등 멀티 프로세스면per_pid_file=True({app_name}.{pid}.log) 필수. - 재호출 시 자신이 부착한 핸들러만 교체하고 다른 루트 핸들러(OTel·Sentry 등)는 보존한다
(
clear_root_handlers=True면 전부 제거 — 이전 버전의 동작). 기본 포맷에[traceId]가 들어간다(include_trace_id=False로 이전 포맷). level기본값은logging.DEBUG— 운영은logging.INFO권장.install_secret_redaction()은configure_logging이후에 호출(재구성마다 다시 호출).
- 기본 파일명(
- 인메모리 상태(
install_rate_limit,install_idempotency기본 스토어,TtlCache,CircuitBreaker)는 프로세스(워커) 단위다 — 인스턴스 간 공유가 필요하면 분산 스토어를IdempotencyStoreProtocol 로 구현해store=로 주입한다. CircuitBreaker를 수동 게이트(allow_request+ 보고)로 쓸 때 보고가 누락되면 HALF_OPEN 프로브가 풀리지 않는다 —call/call_async를 쓰면 보고가 보장된다.jwt는 HS256 시크릿이 32바이트(UTF-8) 미만이면ValueError(fail-fast,allow_weak_secret=True는 교체 전 임시 옵트아웃).log_redact는 정규식 기반 best-effort — 미탐·오탐이 공존한다.log_sanitize와 병용 시 반드시 redact → sanitize(절단) 순서.rrn.is_valid_rrn은 체크섬을 검사하지 않는다 — 2020-10 이후 발급분은 뒷자리가 임의번호라 mod-11 이 성립하지 않는다(checksum_ok_legacy는 그 이전 발급분 전용). 저장 시 암호화 의무.text.chosung.of()의 공백 스킵은 스페이스/탭만(Java 파리티).
버전·릴리스
버전의 단일 소스는 rscc_common.__version__ 이다. py-vX.Y.Z 태그 push 시 CI 가 태그와
버전 정합을 검증한 뒤 PyPI 에 배포한다(Trusted Publishing). 변경 이력은 소스 저장소의
CHANGELOG.md 에 있다.
Metadata
Release files for rscc-common 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| rscc_common-0.4.0.tar.gz | 156.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rscc_common-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 277.3 kB
Release files / rscc_common-0.4.0.tar.gz
| Download URL | rscc_common-0.4.0.tar.gz |
|---|---|
| Size | 156.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dd681b1cc378d4a459a9f748f9dd1da0203f954239141b6b25e416ff83f8c835
|
|
BLAKE2b-256 checksum How to use checksums |
8ffa5f8def613d02b63180e0389810413064c2ccef31775dec6a4faafabd741c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / rscc_common-0.4.0-py3-none-any.whl
| Download URL | rscc_common-0.4.0-py3-none-any.whl |
|---|---|
| Size | 120.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dd153a11e12c87218539989efdf6d982c127ed4c7c44c82fdfc613c8bef1962b
|
|
BLAKE2b-256 checksum How to use checksums |
76237034733ed9d9e54ce18d4a94bfe933785ca9144cf4546715370798393740
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logSecurity for rscc-common 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for rscc_common-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.