diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 23b29b0..5fd06bf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: "3.7" + python-version: "3.8" - uses: actions/cache@v2 id: cache with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b8aef59..b8b771c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,10 +5,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.8 - name: Install Poetry run: | python -m pip install poetry @@ -22,7 +22,7 @@ jobs: strategy: max-parallel: 8 matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa80cdd..377af8f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,32 +1,26 @@ repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 22.6.0 - hooks: - - id: black - - repo: https://github.com/pycqa/autoflake - rev: v1.4 - hooks: - - id: autoflake - args: - - --in-place - - --remove-unused-variables - - --remove-all-unused-imports - - --expand-star-imports - - repo: https://github.com/pycqa/flake8 - rev: 5.0.4 - hooks: - - id: flake8 - args: [--max-line-length=120] - - repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - name: isort (python) +- repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: 'v0.0.254' + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-yaml + args: [--allow-multiple-documents] + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 23.11.0 + hooks: + - id: black + args: [--line-length=120] +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) default_language_version: - python: python3.10 + python: python3.11 diff --git a/README.md b/README.md index 4e325e7..8fb1dc0 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ pip install rcs-pydantic ## Dependency -- python3.x (3.7 이상) +- python3.x (3.8 이상) - pydantic ## Quick start diff --git a/poetry.lock b/poetry.lock index c0231a4..f8af70e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,74 +1,59 @@ [[package]] -name = "anyio" -version = "3.6.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8" [package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} [[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.5" +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +doc = ["packaging", "Sphinx (>=7)", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["anyio", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "21.11b1" +version = "23.12.1" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8" [package.dependencies] -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0,<1" +packaging = ">=22.0" +pathspec = ">=0.9.0" platformdirs = ">=2" -regex = ">=2021.4.4" -tomli = ">=0.2.6,<2.0.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = [ - {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, - {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, -] +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4,!=3.9.0)", "aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -76,23 +61,22 @@ python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" -version = "6.5.0" +version = "7.4.0" description = "Code coverage measurement for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} @@ -100,19 +84,30 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "factory-boy" -version = "3.2.1" +version = "3.3.0" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] Faker = ">=0.7.0" [package.extras] -dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] +dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "sqlalchemy-utils", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"] doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] @@ -126,39 +121,22 @@ python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.4" text-unidecode = "1.3" -typing-extensions = {version = ">=3.10.0.2", markers = "python_version < \"3.8\""} [[package]] name = "fastapi" -version = "0.85.0" +version = "0.109.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.20.4" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.35.0,<0.36.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] -dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.7.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-orjson (==3.6.2)", "types-ujson (==5.4.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] - -[[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "h11" @@ -168,214 +146,153 @@ category = "dev" optional = false python-versions = ">=3.7" -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "isort" -version = "5.10.1" +version = "5.13.2" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.8.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" +colors = ["colorama (>=0.4.6)"] [[package]] name = "mypy" -version = "0.931" +version = "1.8.0" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = ">=1.1.0" -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [[package]] name = "packaging" -version = "21.3" +version = "23.2" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.10.1" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.24)", "sphinx (>=7.1.1)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest (>=7.4)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +python-versions = ">=3.8" [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pydantic" -version = "1.10.2" -description = "Data validation and settings management using python type hints" +version = "2.5.3" +description = "Data validation using Python type hints" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = ">=4.1.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] [[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" +name = "pydantic-core" +version = "2.14.6" +description = "" +category = "main" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.7" -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pytest" -version = "6.2.5" +version = "7.4.4" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" -version = "3.0.0" +version = "4.1.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} @@ -396,12 +313,12 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" six = ">=1.5" [[package]] -name = "regex" -version = "2022.9.13" -description = "Alternative regular expression module, to replace re." +name = "ruff" +version = "0.1.14" +description = "An extremely fast Python linter and code formatter, written in Rust." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "six" @@ -421,18 +338,18 @@ python-versions = ">=3.7" [[package]] name = "starlette" -version = "0.20.4" +version = "0.35.1" description = "The little ASGI library that shines." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [package.dependencies] anyio = ">=3.4.0,<5" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] name = "text-unidecode" @@ -442,37 +359,21 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" -version = "1.2.3" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false -python-versions = ">=3.6" - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" [[package]] name = "uvicorn" @@ -485,68 +386,46 @@ python-versions = ">=3.7" [package.dependencies] click = ">=7.0" h11 = ">=0.8" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] -[[package]] -name = "zipp" -version = "3.9.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] - [metadata] lock-version = "1.1" -python-versions = "^3.7" -content-hash = "932bcf32e8bca5196450bf6100ece0315ac3652effecd2bd07f018dfac42683b" +python-versions = "^3.8" +content-hash = "c9b5ce64682863a8122ada94810f2908ac2f5405394896a38719d0abb43b4e9e" [metadata.files] +annotated-types = [] anyio = [] -atomicwrites = [] -attrs = [] black = [] click = [] colorama = [] coverage = [] +exceptiongroup = [] factory-boy = [] faker = [] fastapi = [] -flake8 = [] h11 = [] idna = [] -importlib-metadata = [] iniconfig = [] isort = [] -mccabe = [] mypy = [] mypy-extensions = [] packaging = [] pathspec = [] platformdirs = [] pluggy = [] -py = [] -pycodestyle = [] pydantic = [] -pyflakes = [] -pyparsing = [] +pydantic-core = [] pytest = [] pytest-cov = [] python-dateutil = [] -regex = [] +ruff = [] six = [] sniffio = [] starlette = [] text-unidecode = [] -toml = [] tomli = [] -typed-ast = [] typing-extensions = [] uvicorn = [] -zipp = [] diff --git a/pyproject.toml b/pyproject.toml index de33aad..ae962ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "rcs-pydantic" -version = "1.0.0" +version = "2.0.0" description = "" authors = ["xncbf "] keywords = ["pydantic", "rcs", "fastapi"] @@ -10,20 +10,20 @@ license = "MIT" readme = "README.md" [tool.poetry.dependencies] -python = "^3.7" -pydantic = "^1.9.0" +python = "^3.8" +pydantic = "^2.0.0" [tool.poetry.dev-dependencies] -pytest-cov = "^3.0.0" -pytest = "^6.2.5" -black = "21.11b1" -flake8 = "^4.0.1" -mypy = "^0.931" -isort = "^5.10.1" +pytest-cov = "*" +pytest = "*" +black = "*" +mypy = "*" +isort = "*" Faker = "^11.3.0" factory-boy = "^3.2.1" -fastapi = "^0.85.0" +fastapi = "^0.109.0" uvicorn = "^0.18.3" +ruff = "^0.1.14" [tool.black] line-length = 120 @@ -36,14 +36,87 @@ extend-exclude = ''' ''' [tool.mypy] +plugins = [ + "pydantic.mypy" +] python_version = '3.10' ignore_missing_imports = 'True' +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +check_untyped_defs = true +no_implicit_reexport = true + + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + [tool.isort] profile = "black" line_length = 120 sections= ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +[tool.ruff] +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "B008", # do not perform function calls in argument defaults + "B023", + "C901" +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "B", "C", "D", "E", "F"] +unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "migrations" +] + +# Same as Black. +line-length = 120 + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.8. +target-version = "py38" + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + # https://docs.pytest.org/en/6.2.x/reference.html [tool.pytest.ini_options] minversion = "6.2.5" diff --git a/rcs_pydantic/enums.py b/rcs_pydantic/enums.py index 70b12ad..6282bc0 100644 --- a/rcs_pydantic/enums.py +++ b/rcs_pydantic/enums.py @@ -43,7 +43,7 @@ class HeaderEnum(Enum): class ActionEnum(Enum): URL_ACTION: str = "urlAction" # 단말기에 기본 웹 브라우저로 설정된 앱을 통해서, 웹페이지로 이동할 수있습니다 - LOCAL_BROWSER_ACTION: str = "localBrowserAction" # 단말기의 메시지 앱 내부 브라우저를 통해 웹페이지로 이동할 수 있습니다 + LOCAL_BROWSER_ACTION: str = "localBrowserAction" # 단말기의 메시지 앱 내부 브라우저를 통해 웹페이지로 이동할 수 있습니다 # noqa: E501 MAP_ACTION: str = "mapAction" # 미리 지정된 위치를 보여주거나 사용자의 현재 위치를 서버로 전송 할 수 있습니다. CALENDAR_ACTION: str = "calendarAction" # 사용자의 캘린더에 특정 일정을 등록 할 수 있습니다. CLIPBOARD_ACTION: str = "clipboardAction" # 특정 문구를 사용자 단말이 자동으로 복사 할 수 있게 합니다 diff --git a/rcs_pydantic/errors.py b/rcs_pydantic/errors.py index 0dc10bd..4605a11 100644 --- a/rcs_pydantic/errors.py +++ b/rcs_pydantic/errors.py @@ -58,7 +58,10 @@ class ErrorCodeEnum(TupleEnum): RCS_USER_NOT_FOUND = (41110, "Message Status Webhook ") PROCESS_REVOCATION_REQUEST_FAILED = (41117, "Message Status Webhook ") USER_FOR_MESSAGE_RECEIVING_NOT_FOUND = (41200, "") - MESSAGE_NOT_ACCEPTABLE = (41201, "Message Session형성과정 중 단말의 SDP에 message송신에 필요한 feature tag가 없는 경우 발생") + MESSAGE_NOT_ACCEPTABLE = ( + 41201, + "Message Session형성과정 중 단말의 SDP에 message송신에 필요한 feature tag가 없는 경우 발생", + ) # noqa: E501 USER_IS_NOT_CAPABLE_FOR_TEXT = (41210, "Bot이 Text message를 보낼 때 user의 capability에 chat이 없는 경우 발생.") USER_IS_NOT_CAPABLE_FOR_FT = (41211, "Bot이 File message를 보낼 때 user의 capability에 fthttp가 없는 경우 발생") USER_IS_NOT_CAPABLE_FOR_RICHCARD = ( @@ -73,7 +76,7 @@ class ErrorCodeEnum(TupleEnum): 41221, "clipboardAction, localBrowserAction, messageHeader, messageFooter, openrichcard, geolocationPushMessage," "copyAllowed Bot이 v1.1에 해당하는 Extended message를 보낼 때 user의 capability에 bot, chatbot.sa, xbotmessage " - "1.1이 없는 경우 발생. 이 때 xbotmessage는 상위 version이 하위 version을 포함한다. (1.2 version이 있다면 1.1에 해당하는 " + "1.1이 없는 경우 발생. 이 때 xbotmessage는 상위 version이 하위 version을 포함한다. (1.2 version이 있다면 1.1에 해당하는 " # noqa: E501 "capability를 가진 것으로 간주함)", ) USER_IS_NOT_CAPABLE_FOR_XBOTMESSAGE_1_2 = ( @@ -83,10 +86,16 @@ class ErrorCodeEnum(TupleEnum): ) USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_0 = ( 41230, - "Bot이 v1.0에 해당하는 Openrichcard를 보낼 때 user의 capability에 bot, chatbot.sa, xbotmessage 1.0이 없는 경우 발생", + "Bot이 v1.0에 해당하는 Openrichcard를 보낼 때 user의 capability에 bot, chatbot.sa, xbotmessage 1.0이 없는 경우 발생", # noqa: E501 ) - USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_1 = (41231, "v1.1에 해당하는 Openrichcard가 현재 정의 및 사용되지 않으므로, 실제 발생하지 않는 code") - USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_2 = (41232, "v1.2에 해당하는 Openrichcard가 현재 정의 및 사용되지 않으므로, 실제 발생하지 않는 code") + USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_1 = ( + 41231, + "v1.1에 해당하는 Openrichcard가 현재 정의 및 사용되지 않으므로, 실제 발생하지 않는 code", + ) # noqa: E501 + USER_IS_NOT_CAPABLE_FOR_OPENRICHARD_1_2 = ( + 41232, + "v1.2에 해당하는 Openrichcard가 현재 정의 및 사용되지 않으므로, 실제 발생하지 않는 code", + ) # noqa: E501 USER_IS_NOT_CAPABLE_FOR_GEOLOCATION_PUSH_REQUEST = (41240, "User is not capable for GEOLOCATION PUSH REQUEST") FAILED_TO_GET_MESSAGE_CONTENT_TYPE = (41250, "Failed to get message content type") FILE_DOWNLOAD_FAILED = (41300, "") @@ -275,7 +284,7 @@ class MaaPErrorCodeEnum(TupleEnum): PERSISTENT_MENU_PERMISSION_ERROR = (52009, "Persistent Menu 등록이 허용되지 않습니다.") INVALID_PERSISTENT_MENU_DATA = (52010, "Persistent menu JSON 데이터 오류") MESSAGE_TRANSMISSION_TIME_EXCEEDING = (52016, "실시간 메시지 인입 후 10초안에 삼성으로 전달되지 못함") - MESSAGEBASE_ID_STOPPED_TEMPORARILY = (52023, "메시지 베이스의 상태가 'pause'인 메시지 베이스 메시지로 전문 구성하여 전송 시도 시") + MESSAGEBASE_ID_STOPPED_TEMPORARILY = (52023, "메시지 베이스의 상태가 'pause'인 메시지 베이스 메시지로 전문 구성하여 전송 시도 시") # noqa: E501 INVALID_WEBHOOK_REQUEST_PARAMETER = (52101, "잘못된 Webhook 중계사 요청 파라미터 입니다.") WEBHOOK_HOST_CONNECT_ERROR = (52102, "Webhook 중계 시스템 연결 오류") WEBHOOK_HOST_SERVER_REQUEST_FAILURE = (52103, "중계사 Webhook 전송 요청을 실패 했습니다.") @@ -293,7 +302,7 @@ class MaaPErrorCodeEnum(TupleEnum): INVAILD_CONTACT_NUMBER_USER = (54001, "자사 고객이 아닙니다.") NO_RCS_CAPABILITY = (54002, "자사 고객이지만, RCS메시지를 수신할 수 있는 가입자가 아닙니다.") UNABLE_SENDING_TO_RECIPIENT = (54003, "단말기기로 RCS 메시지를 전송할 수 없습니다.") - MAA_P_INTERNAL_ERROR = (54004, "MaaP 시스템 혹은 RCS 프로토콜 상의 이슈로 발송 실패되었음 (삼성 에러 40001 ~ 41100, 42601)") + MAA_P_INTERNAL_ERROR = (54004, "MaaP 시스템 혹은 RCS 프로토콜 상의 이슈로 발송 실패되었음 (삼성 에러 40001 ~ 41100, 42601)") # noqa: E501 CORP_CONTENT_ERROR = (55001, "기업 정보 내용이 누락된 필수항목이 있습니다.") INVALID_PROPERTY = (55002, "필수 파라미터 검증 오류") AGENCY_CONTENT_ERROR = (55101, "대행사 정보 내용이 누락된 필수 항목이 있습니다.") @@ -326,7 +335,10 @@ class MaaPErrorCodeEnum(TupleEnum): INVALID_MESSAGEBASEFORM_ID = (55602, "messagebaseformID가 존재하지 않습니다.") INVALID_MESSAGE_BASE_PRODUCT_CODE = (55603, "messaegBase의 상품코드 에러") PROHIBITED_TEXT_CONTENT = (55701, "(광고) 를 사용할 수 없음") - ACTION_BUTTON_PERMISSION_ERROR = (55702, "Action button이 허용되지 않는 messagebaseID에서 Action button을 사용하였음") + ACTION_BUTTON_PERMISSION_ERROR = ( + 55702, + "Action button이 허용되지 않는 messagebaseID에서 Action button을 사용하였음", + ) # noqa: E501 PROHIBITED_HEADER_VALUE = (55703, "허용되지 않은 header 값 사용") PROHIBITED_FOOTER_FIELD = (55704, "header 값과 일치 하지 않은 footer 사용 (ex. header가 0 인데, footer 가 있음)") MISSING_FOOTER_CONTENT = (55705, "footer값이 누락되어 있습니다 (ex. header가 1 인데, footer 가 없음)") @@ -421,10 +433,10 @@ class MaaPErrorCodeEnum(TupleEnum): AUTO_REPLY_MESSAGE_SENDING_ERROR = (51933, "자동 응답 메시지 발송 수행 오류가 발생했습니다.") NON_SERVICE_SUPPORTED_ERROR = (51934, "처리 미 대상 서비스 입니다.") SAMSUNG_MAA_P_CORE_FILE_SERVER_CONNECTION_ERROR = (51935, "삼성 MaaP Core 파일 서버 연결 오류가 발생했습니다.") - MESSAGE_FILE_MESSAGE_EVENT_FILE_DOWNLOAD_ERROR_1 = (51936, "파일 메시지 이벤트의 파일 메시지 다운로드 수행 오류가 발생했습니다.") - MESSAGE_FILE_MESSAGE_EVENT_FILE_DOWNLOAD_ERROR_2 = (51937, "파일 메시지 이벤트의 파일 메시지 다운로드 수행 오류가 발생했습니다.") - MESSAGE_FILE_MESSAGE_REGISTRATION_TO_DB_ERROR = (51938, "파일 메시지 이벤트의 파일 정보 DB 등록 오류가 발생했습니다.") - MESSAGE_FILE_MESSAGE_REGISTRATION_TO_DB_FAILURE = (51939, "파일 메시지 이벤트의 파일 정보 DB 등록 작업을 실패 했습니다.") + MESSAGE_FILE_MESSAGE_EVENT_FILE_DOWNLOAD_ERROR_1 = (51936, "파일 메시지 이벤트의 파일 메시지 다운로드 수행 오류가 발생했습니다.") # noqa: E501 + MESSAGE_FILE_MESSAGE_EVENT_FILE_DOWNLOAD_ERROR_2 = (51937, "파일 메시지 이벤트의 파일 메시지 다운로드 수행 오류가 발생했습니다.") # noqa: E501 + MESSAGE_FILE_MESSAGE_REGISTRATION_TO_DB_ERROR = (51938, "파일 메시지 이벤트의 파일 정보 DB 등록 오류가 발생했습니다.") # noqa: E501 + MESSAGE_FILE_MESSAGE_REGISTRATION_TO_DB_FAILURE = (51939, "파일 메시지 이벤트의 파일 정보 DB 등록 작업을 실패 했습니다.") # noqa: E501 WEBHOOK_SCHEDULER_ASYNC_EXECUTION_ERROR = (51950, "Webhook 스케줄러 비동기 수행 오류.") WEBHOOK_SCHEDULER_DB_EXECUTION_ERROR = (51951, "Webhook 스케줄러 DB 수행 오류.") WEBHOOK_SCHEDULER_DB_EXECUTION_FAILUER = (51952, "Webhook 스케줄러 DB 수행 실패.") @@ -567,7 +579,7 @@ class KTErrorCodeEnum(TupleEnum): NOT_FOUND_RCS_SUBSCRIBER = (76003, "RCS 가입 정보 없음") XROSHOT_SENDER_INTERNAL_ERROR = (76004, "Xroshot Sender 내부 에러") XROSHOT_MANAGER_INTERNAL_ERROR = (76005, "Xroshot Manager 내부 에러") - EXPIRED_MESSAGE_RECEIVED_TIME = (77001, "Legacy : 레포트 수신 시간 만료 (메시지 전송 후, 24시간 레포트 못받는 경우) RCS 3일") + EXPIRED_MESSAGE_RECEIVED_TIME = (77001, "Legacy : 레포트 수신 시간 만료 (메시지 전송 후, 24시간 레포트 못받는 경우) RCS 3일") # noqa: E501 INVALIED_MESSAGE_SEQUENCE = (77002, "Message Sequence Number가 틀린 경우") NON_EXISTING_WEBHOOK_MESSAGE = (77003, "Webhook 발송 메시지가 존재하지 않습니다") INVALID_WEBHOOK_MESSAGE_1 = (77004, "잘못된 Webhook 발송 메시지입니다.") diff --git a/rcs_pydantic/main.py b/rcs_pydantic/main.py index 8bd33d5..365e04f 100644 --- a/rcs_pydantic/main.py +++ b/rcs_pydantic/main.py @@ -77,17 +77,21 @@ def make_common_info(self, message_info: scheme.MessageInfo) -> dict: else: msg_service_type = enums.MessageServiceTypeEnum.RCS - common = { - "msgId": str(uuid.uuid4()), - "userContact": message_info.userContact, - "scheduleType": 0, - "msgServiceType": msg_service_type, - } - if self.message_group_id: - common["msgGroupId"] = self.message_group_id - - return scheme.CommonInfo(**common).dict(exclude_unset=True) + return scheme.CommonInfo( + msgId=str(uuid.uuid4()), + userContact=str(message_info.userContact), + scheduleType=enums.ScheduleTypeEnum.IMMEDIATE, + msgServiceType=msg_service_type, + msgGroupId=self.message_group_id, + ).model_dump(exclude_none=True) + else: + return scheme.CommonInfo( + msgId=str(uuid.uuid4()), + userContact=str(message_info.userContact), + scheduleType=enums.ScheduleTypeEnum.IMMEDIATE, + msgServiceType=msg_service_type, + ).model_dump(exclude_none=True) def make_rcs_info(self, message_info: scheme.MessageInfo) -> dict: rcs_info = scheme.RcsInfo( @@ -117,7 +121,4 @@ def make_rcs_info(self, message_info: scheme.MessageInfo) -> dict: rcs_info.chipList = self.chips if message_info.replyId: rcs_info.replyId = message_info.replyId - return rcs_info.dict(exclude_unset=True) - - def send(self): - self.send_info + return rcs_info.model_dump(exclude_none=True) diff --git a/rcs_pydantic/scheme.py b/rcs_pydantic/scheme.py index 86cc0c8..c27a5bf 100644 --- a/rcs_pydantic/scheme.py +++ b/rcs_pydantic/scheme.py @@ -1,137 +1,124 @@ from typing import Dict, List, Optional, Union -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, ConfigDict, Field, validator +from typing_extensions import Never from . import enums -class EmptyDict(BaseModel): - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def validate(cls, v): - if not isinstance(v, dict): - raise TypeError("dict required") - if len(v.keys()) > 0: - raise ValueError("dict must be empty") - return {} - - class RcsSMSBody(BaseModel): - title: Optional[str] = Field(max_length=30) + title: Optional[str] = Field(max_length=30, default=None) description: str = Field(max_length=100) - media: Optional[str] + media: Optional[str] = None class RcsLMSBody(BaseModel): - title: Optional[str] = Field(max_length=30) + title: Optional[str] = Field(max_length=30, default=None) description: str = Field(max_length=1300) - media: Optional[str] + media: Optional[str] = None class RcsMMSBody(BaseModel): - title: Optional[str] = Field(max_length=30) + title: Optional[str] = Field(max_length=30, default=None) description: str = Field(max_length=1300) - media: Optional[str] + media: Optional[str] = None class RcsCHATBody(BaseModel): - title: Optional[str] = Field(max_length=30) + title: Optional[str] = Field(max_length=30, default=None) description: str = Field(max_length=1300) - media: Optional[str] + media: Optional[str] = None class RcsSMSCarouselBody(BaseModel): - title1: Optional[str] = Field(max_length=30) + title1: Optional[str] = Field(max_length=30, default=None) description1: str = Field(max_length=100) - media1: Optional[str] - title2: Optional[str] = Field(max_length=30) - description2: Optional[str] = Field(max_length=100) - media2: Optional[str] + media1: Optional[str] = None + title2: Optional[str] = Field(max_length=30, default=None) + description2: Optional[str] = Field(max_length=100, default=None) + media2: Optional[str] = None title3: Optional[str] = Field(max_length=30) - description3: Optional[str] = Field(max_length=100) - media3: Optional[str] + description3: Optional[str] = Field(max_length=100, default=None) + media3: Optional[str] = None title4: Optional[str] = Field(max_length=30) description4: Optional[str] = Field(max_length=100) - media4: Optional[str] + media4: Optional[str] = None title5: Optional[str] = Field(max_length=30) description5: Optional[str] = Field(max_length=100) - media5: Optional[str] + media5: Optional[str] = None title6: Optional[str] = Field(max_length=30) description6: Optional[str] = Field(max_length=100) - media6: Optional[str] + media6: Optional[str] = None class RcsLMSCarouselBody(BaseModel): title1: Optional[str] = Field(max_length=30) description1: str = Field(max_length=1300) - media1: Optional[str] - title2: Optional[str] = Field(max_length=30) - description2: Optional[str] = Field(max_length=1300) - media2: Optional[str] - title3: Optional[str] = Field(max_length=30) - description3: Optional[str] = Field(max_length=1300) - media3: Optional[str] - title4: Optional[str] = Field(max_length=30) - description4: Optional[str] = Field(max_length=1300) - media4: Optional[str] - title5: Optional[str] = Field(max_length=30) - description5: Optional[str] = Field(max_length=1300) - media5: Optional[str] - title6: Optional[str] = Field(max_length=30) - description6: Optional[str] = Field(max_length=1300) - media6: Optional[str] + media1: Optional[str] = None + title2: Optional[str] = Field(max_length=30, default=None) + description2: Optional[str] = Field(max_length=1300, default=None) + media2: Optional[str] = None + title3: Optional[str] = Field(max_length=30, default=None) + description3: Optional[str] = Field(max_length=1300, default=None) + media3: Optional[str] = None + title4: Optional[str] = Field(max_length=30, default=None) + description4: Optional[str] = Field(max_length=1300, default=None) + media4: Optional[str] = None + title5: Optional[str] = Field(max_length=30, default=None) + description5: Optional[str] = Field(max_length=1300, default=None) + media5: Optional[str] = None + title6: Optional[str] = Field(max_length=30, default=None) + description6: Optional[str] = Field(max_length=1300, default=None) + media6: Optional[str] = None class RcsMMSCarouselBody(BaseModel): title1: Optional[str] = Field(max_length=30) description1: str = Field(max_length=1300) - media1: Optional[str] - title2: Optional[str] = Field(max_length=30) - description2: Optional[str] = Field(max_length=1300) - media2: Optional[str] - title3: Optional[str] = Field(max_length=30) - description3: Optional[str] = Field(max_length=1300) - media3: Optional[str] - title4: Optional[str] = Field(max_length=30) - description4: Optional[str] = Field(max_length=1300) - media4: Optional[str] - title5: Optional[str] = Field(max_length=30) - description5: Optional[str] = Field(max_length=1300) - media5: Optional[str] - title6: Optional[str] = Field(max_length=30) - description6: Optional[str] = Field(max_length=1300) - media6: Optional[str] + media1: Optional[str] = None + title2: Optional[str] = Field(max_length=30, default=None) + description2: Optional[str] = Field(max_length=1300, default=None) + media2: Optional[str] = None + title3: Optional[str] = Field(max_length=30, default=None) + description3: Optional[str] = Field(max_length=1300, default=None) + media3: Optional[str] = None + title4: Optional[str] = Field(max_length=30, default=None) + description4: Optional[str] = Field(max_length=1300, default=None) + media4: Optional[str] = None + title5: Optional[str] = Field(max_length=30, default=None) + description5: Optional[str] = Field(max_length=1300, default=None) + media5: Optional[str] = None + title6: Optional[str] = Field(max_length=30, default=None) + description6: Optional[str] = Field(max_length=1300, default=None) + media6: Optional[str] = None class RcsCHATCarouselBody(BaseModel): title1: Optional[str] = Field(max_length=30) description1: str = Field(max_length=1300) - media1: Optional[str] - title2: Optional[str] = Field(max_length=30) - description2: Optional[str] = Field(max_length=1300) - media2: Optional[str] - title3: Optional[str] = Field(max_length=30) - description3: Optional[str] = Field(max_length=1300) - media3: Optional[str] - title4: Optional[str] = Field(max_length=30) - description4: Optional[str] = Field(max_length=1300) - media4: Optional[str] - title5: Optional[str] = Field(max_length=30) - description5: Optional[str] = Field(max_length=1300) - media5: Optional[str] - title6: Optional[str] = Field(max_length=30) - description6: Optional[str] = Field(max_length=1300) - media6: Optional[str] + media1: Optional[str] = None + title2: Optional[str] = Field(max_length=30, default=None) + description2: Optional[str] = Field(max_length=1300, default=None) + media2: Optional[str] = None + title3: Optional[str] = Field(max_length=30, default=None) + description3: Optional[str] = Field(max_length=1300, default=None) + media3: Optional[str] = None + title4: Optional[str] = Field(max_length=30, default=None) + description4: Optional[str] = Field(max_length=1300, default=None) + media4: Optional[str] = None + title5: Optional[str] = Field(max_length=30, default=None) + description5: Optional[str] = Field(max_length=1300, default=None) + media5: Optional[str] = None + title6: Optional[str] = Field(max_length=30, default=None) + description6: Optional[str] = Field(max_length=1300, default=None) + media6: Optional[str] = None class LocationInfo(BaseModel): - query: Optional[str] - longitude: Optional[float] - latitude: Optional[float] - label: Optional[str] + query: Optional[str] = None + longitude: Optional[float] = None + latitude: Optional[float] = None + label: Optional[str] = None class ShowLocationInfo(BaseModel): @@ -147,7 +134,7 @@ class PostParameterInfo(BaseModel): class OpenUrlInfo(BaseModel): url: str isHalfView: Optional[str] = Field(default="false") - postParameter: Optional[PostParameterInfo] + postParameter: Optional[PostParameterInfo] = None class CreateCalendarEventInfo(BaseModel): @@ -191,8 +178,8 @@ class MapActionInfo(BaseModel): 미리 지정된 위치를 보여주거나 사용자의 현재 위치를 서버로 전송 할 수 있습니다. """ - showLocation: Optional[ShowLocationInfo] - requestLocationPush: Optional[Dict] + showLocation: Optional[ShowLocationInfo] = None + requestLocationPush: Optional[Dict] = None class CalendarActionInfo(BaseModel): @@ -241,20 +228,20 @@ class ReplyActionInfo(BaseModel): class ActionInfo(BaseModel): - urlAction: Optional[UrlActionInfo] - localBrowserAction: Optional[LocalBrowserActionInfo] - mapAction: Optional[MapActionInfo] - calendarAction: Optional[CalendarActionInfo] - clipboardAction: Optional[ClipboardActionInfo] - composeAction: Optional[ComposeActionInfo] - dialerAction: Optional[DialerActionInfo] + urlAction: Optional[UrlActionInfo] = None + localBrowserAction: Optional[LocalBrowserActionInfo] = None + mapAction: Optional[MapActionInfo] = None + calendarAction: Optional[CalendarActionInfo] = None + clipboardAction: Optional[ClipboardActionInfo] = None + composeAction: Optional[ComposeActionInfo] = None + dialerAction: Optional[DialerActionInfo] = None displayText: str = Field(max_length=200) postback: PostbackInfo class SuggestionInfo(BaseModel): - action: Optional[ActionInfo] - reply: Optional[ReplyActionInfo] + action: Optional[ActionInfo] = None + reply: Optional[ReplyActionInfo] = None class ButtonInfo(BaseModel): @@ -271,14 +258,16 @@ class CommonInfo(BaseModel): msgId: str = Field(max_length=40) userContact: str = Field(max_length=40) - scheduleType: Optional[enums.ScheduleTypeEnum] - msgGroupId: Optional[str] = Field(max_length=20) + scheduleType: Optional[enums.ScheduleTypeEnum] = None + msgGroupId: Optional[str] = Field(max_length=20, default=None) msgServiceType: enums.MessageServiceTypeEnum class RcsInfo(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + chatbotId: str = Field(max_length=40) - agencyId: Optional[str] = Field(max_length=20) + agencyId: Optional[str] = Field(max_length=20, default=None) """ # agencyId 대행사 ID @@ -291,19 +280,21 @@ class RcsInfo(BaseModel): """ # agencyKey agencyId (대행사 ID) 와 매핑되는 대행사 Key 값 - [보안성 강화] agencyId - agencyKey 가 불일치 하는 경우, 통신사에서 실패 처리. 대행사 고객인 경우 필수값. * agencyKey 는 RBC 에서 발급 및 갱신 가능하며, + [보안성 강화] agencyId - agencyKey 가 불일치 하는 경우, + 통신사에서 실패 처리. 대행사 고객인 경우 필수값. * agencyKey 는 RBC 에서 발급 및 갱신 가능하며, 갱신시 기존(old) agencyKey 는 최대 24 시간 유효함 """ brandKey: str """ # brandKey chatbotId (챗봇 ID) 소유 brandId (브랜드 ID) 와 매핑되는 브랜드 Key 값 - [보안성 강화] brandId - brandKey 가 불일치 하는 경우, 통신사에서 실패 처리. 대행사 고객인 경우 필수값. *brandKey 는 기존 RBC 에서 발급, 제공 중인 값 + [보안성 강화] brandId - brandKey 가 불일치 하는 경우, 통신사에서 실패 처리. + 대행사 고객인 경우 필수값. *brandKey 는 기존 RBC 에서 발급, 제공 중인 값 """ messagebaseId: Union[enums.MessageEnum, enums.RCSMessageEnum, str] serviceType: enums.ServiceTypeEnum - expiryOption: Optional[enums.ExpiryOptionEnum] + expiryOption: Optional[enums.ExpiryOptionEnum] = None """ # expiryOption 메시지 처리 옵션 enum: [1, 2] @@ -316,7 +307,7 @@ class RcsInfo(BaseModel): """ header: enums.HeaderEnum - footer: Optional[str] = Field(max_length=20, regex=r"^[\d-]*$") + footer: Optional[str] = Field(max_length=20, pattern=r"^[\d-]*$", default=None) """ # footer 수신거부 전화번호 (숫자, - 만 가능, Max: 20 자리) @@ -332,7 +323,7 @@ def footer_validator(cls, v, values, **kwargs): raise ValueError("If header is 1 then footer should be provided.") return v - cdrId: Optional[str] + cdrId: Optional[str] = None """ # cdrId 청약 ID 기록시 기록한 ID로 과금 처리 된다. @@ -383,7 +374,7 @@ def footer_validator(cls, v, values, **kwargs): - "media" :"maapfile://{fileId}" """ - buttons: Optional[List[Union[ButtonInfo, EmptyDict]]] + buttons: Optional[List[Union[ButtonInfo, Dict[Never, Never]]]] = None """ # buttons GSMA RCC.07의3.6.10.4의 ‘suggestions’ 규격에 준하여 버튼을 구성 @@ -397,7 +388,7 @@ def footer_validator(cls, v, values, **kwargs): 버튼의 변수부를 등록 하여 사용하며, 해당 필드 허용하지 않음. """ - chipList: Optional[List[SuggestionInfo]] + chipList: Optional[List[SuggestionInfo]] = None """ # chipList GSMA RCC.07의3.6.10.4의 ‘suggestion’ 규격에 따라 chiplist를 구성(RCC.07의 기준 버전 확인 필요) @@ -405,18 +396,17 @@ def footer_validator(cls, v, values, **kwargs): * Chiplist는 최대 11개까지 사용 가능하다 """ - replyId: Optional[str] = Field(max_length=40) + replyId: Optional[str] = Field(max_length=40, default=None) """ # replyId 양기업의 양방향 momsg를 수신할 때 포함된 replyID를 그대로 넣어서 전송한다 Maximum : 40Byte ㅇRCSCHAT인 경우에 반드시 포함되어야 하며, RCSCHAT이 아닌 경우에는 포함되면 발송이 실패된다 - *양방향서비스에서 양방향 대화의 세션을 관리하는 기준으로, 고객이 양방향 MO를 수행할 때마다 새롭게 할당되며, 유효시간은 24시간임. 유효시간이 만료된 replyId포함되어 발송되면 실패처리된다 + *양방향서비스에서 양방향 대화의 세션을 관리하는 기준으로, + 고객이 양방향 MO를 수행할 때마다 새롭게 할당되며, 유효시간은 24시간임. + 유효시간이 만료된 replyId포함되어 발송되면 실패처리된다 """ - class Config: - smart_union = True - class LegacyInfo(BaseModel): """ @@ -429,7 +419,7 @@ class LegacyInfo(BaseModel): Fallback은SMS/LMS에 대해서만 제공하고, MMS,CHAT에 대해서는 제공하지 않는다 """ callback: str = Field(max_length=20) - subject: Optional[str] = Field(max_length=50) + subject: Optional[str] = Field(max_length=50, default=None) msg: str = Field(max_length=4000) """ SMS : 80Byte @@ -442,7 +432,7 @@ class LegacyInfo(BaseModel): - 0: SMS/LMS인 경우, 미디어파일 없는 경우 - 1~3 : (TBD) MMS인 경우 """ - contentData: Optional[str] = Field(max_length=250) + contentData: Optional[str] = Field(max_length=250, default=None) """ (TBD) MMS 에서 사용하는 이미지파일의 정보 - 컨텐츠위치^컨텐츠타입^컨텐츠서브타입 @@ -456,13 +446,13 @@ class LegacyInfo(BaseModel): 예) http://10.217.59.209:5084/data/MEDIA/RCS/send/2021/08/09/test004.jpg^1^JPG Maximum : 250Byte """ - prefix: Optional[str] = Field(max_length=10) + prefix: Optional[str] = Field(max_length=10, default=None) """ RCS 전송성공 가능성 있는 fallback 메시지 전송시 삽입문구 - 79998(전송성공불확실함), 55820(Revoked Message)... 등 - SMS 는 msg(본문), LMS/MMS 는 subject(제목)내 문구 삽입 예) 재전송, RE: """ - kisaOrigCode: Optional[int] = Field(ge=0, le=999999999) + kisaOrigCode: Optional[int] = Field(ge=0, le=999999999, default=None) """ 최초 발신 사업자코드, 9 자리 숫자 형식 [보안성 강화] 대행사 고객의 경우 (즉, KT 중계가 최초 발신 @@ -496,21 +486,21 @@ class StatusInfo(BaseModel): 메시지 전송 결과 """ - rcsId: Optional[str] = Field(max_length=20) + rcsId: Optional[str] = Field(max_length=20, default=None) msgId: str = Field(max_length=40) - userContact: Optional[str] = Field(max_length=40) + userContact: Optional[str] = Field(max_length=40, default=None) status: enums.MessageStatusEnum - serviceType: Union[enums.ServiceTypeEnum, enums.LegacyServiceTypeEnum, None] - mnoInfo: Optional[enums.MnoInfoEnum] - sentTime: Optional[str] = Field(regex=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") - reason: Optional[ReasonInfo] - error: Optional[ErrorInfo] - legacyError: Optional[LegacyErrorInfo] - timestamp: str = Field(regex=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") - autoReplyMsgId: Optional[str] = Field(max_length=40) - postbackId: Optional[str] = Field(max_length=40) - chatbotId: Optional[str] = Field(max_length=40) - bill: Optional[enums.BillEnum] + serviceType: Union[enums.ServiceTypeEnum, enums.LegacyServiceTypeEnum, None] = None + mnoInfo: Optional[enums.MnoInfoEnum] = None + sentTime: Optional[str] = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$", default=None) + reason: Optional[ReasonInfo] = None + error: Optional[ErrorInfo] = None + legacyError: Optional[LegacyErrorInfo] = None + timestamp: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") + autoReplyMsgId: Optional[str] = Field(max_length=40, default=None) + postbackId: Optional[str] = Field(max_length=40, default=None) + chatbotId: Optional[str] = Field(max_length=40, default=None) + bill: Optional[enums.BillEnum] = None class QuerystatusInfo(BaseModel): @@ -564,10 +554,10 @@ class MessageInfo(BaseModel): 고객이 단말 대화방을 최초 진입 시 설정됨 Maximum : 10Byte """ - postbackId: Optional[str] = Field(max_length=40) - postbackData: Optional[str] = Field(max_length=2048) - displayText: Optional[str] = Field(max_length=200) - messageBody: Union[TextMessageInfo, UserLocationInfo, FileMessageInfo, None] + postbackId: Optional[str] = Field(max_length=40, default=None) + postbackData: Optional[str] = Field(max_length=2048, default=None) + displayText: Optional[str] = Field(max_length=200, default=None) + messageBody: Union[TextMessageInfo, UserLocationInfo, FileMessageInfo, None] = None """ eventType이message인 경우에만 설정 - 텍스트 메시지의 경우 샘플 @@ -607,13 +597,13 @@ def check_message_body(cls, v, values, **kwargs): userContact: str = Field(max_length=40) chatbotId: str = Field(max_length=40) - timestamp: str = Field(regex=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") + timestamp: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") class SendInfo(BaseModel): common: CommonInfo rcs: RcsInfo - legacy: Optional[LegacyInfo] + legacy: Optional[LegacyInfo] = None class TokenInfo(BaseModel): @@ -628,15 +618,15 @@ class FileRegistInfo(BaseModel): usageService: enums.FileUsageServiceEnum mimeType: str file: bytes - description: Optional[str] + description: Optional[str] = None class FileInfo(BaseModel): - fileId: Optional[str] + fileId: Optional[str] = None usageType: enums.FileUsageTypeEnum usageService: enums.FileUsageServiceEnum mimeType: str status: enums.FileStatusEnum - size: Optional[int] - expiryDate: str = Field(regex=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") + size: Optional[int] = None + expiryDate: str = Field(pattern=r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{2}$") url: str diff --git a/scripts/lint-test.sh b/scripts/lint-test.sh index 0763528..ab5d88a 100644 --- a/scripts/lint-test.sh +++ b/scripts/lint-test.sh @@ -4,4 +4,4 @@ set -x poetry run black rcs_pydantic --check poetry run isort --check-only rcs_pydantic -poetry run flake8 +poetry run ruff check --exit-zero . diff --git a/tests/factory.py b/tests/factory.py index 7c9ec42..d71f957 100644 --- a/tests/factory.py +++ b/tests/factory.py @@ -247,7 +247,7 @@ class Meta: model = scheme.ErrorInfo code: str = factory.LazyAttribute( - lambda n: fake.random_element(elements=[x.value[0] for x in errors.ErrorCodeEnum]) + lambda n: str(fake.random_element(elements=[x.value[0] for x in errors.ErrorCodeEnum])) ) message: str = factory.LazyAttribute(lambda n: fake.sentence(nb_words=20)[:50]) @@ -274,7 +274,6 @@ class Meta: model = scheme.ResponseInfo status: str = "200" - data: dict = factory.SubFactory(TokenInfoFactory) class MessageInfoFactory(factory.Factory): diff --git a/tests/test_factory.py b/tests/test_factory.py index 0491ec2..0452a51 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -130,7 +130,7 @@ def test_token_info_factory(): def test_response_info_factory(): - factory.ResponseInfoFactory() + factory.ResponseInfoFactory(data=factory.TokenInfoFactory().model_dump()) def test_message_info_factory(): diff --git a/tests/test_rcs_pydantic.py b/tests/test_rcs_pydantic.py index 9cba2d5..ac5b126 100644 --- a/tests/test_rcs_pydantic.py +++ b/tests/test_rcs_pydantic.py @@ -1,12 +1,10 @@ import pytest from faker import Faker -from pydantic import BaseModel from rcs_pydantic import enums from rcs_pydantic.errors import ErrorCodeEnum, KTErrorCodeEnum, MaaPErrorCodeEnum, RcsBizCenterErrorCodeEnum from rcs_pydantic.exceptions import MessageException from rcs_pydantic.main import RcsMessage -from rcs_pydantic.scheme import EmptyDict from . import factory @@ -21,14 +19,14 @@ def test_rcs_message(): agency_id="abc", agency_key="abc", brand_key="abc", - expiry_option=2, - header="1", + expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, + header=enums.HeaderEnum.ADVERTISE, footer="010-0000-0000", cdr_id="abc", copy_allowed=True, message_group_id="abc", ) - rcs_message.send() + assert rcs_message.send_info def test_rcs_chat_message(): @@ -39,15 +37,15 @@ def test_rcs_chat_message(): agency_id="abc", agency_key="abc", brand_key="abc", - expiry_option=2, - header="1", + expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, + header=enums.HeaderEnum.ADVERTISE, footer="010-0000-0000", cdr_id="abc", copy_allowed=True, service_type=enums.ServiceTypeEnum.CHAT, chips=[factory.SuggestionInfoFactory()], ) - rcs_message.send() + assert rcs_message.send_info def test_rcs_message_with_empty_button(): @@ -58,13 +56,13 @@ def test_rcs_message_with_empty_button(): agency_id="abc", agency_key="abc", brand_key="abc", - expiry_option=2, - header="1", + expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, + header=enums.HeaderEnum.ADVERTISE, footer="010-0000-0000", cdr_id="abc", copy_allowed=True, ) - rcs_message.send() + assert rcs_message.send_info def test_tuple_enum_has_value(): @@ -104,22 +102,6 @@ def test_unknown_error_code_message_exception(): raise MessageException(123) -def test_empty_dict(): - class C(BaseModel): - item: EmptyDict - - C(item={}) - - with pytest.raises(ValueError): - C(item=None) - - with pytest.raises(ValueError): - C(item=[]) - - with pytest.raises(ValueError): - C(item={"a": 1}) - - def test_rcs_legacy_message(): rcs_message = RcsMessage( factory.MessageInfoFactory(), @@ -128,11 +110,11 @@ def test_rcs_legacy_message(): agency_id="abc", agency_key="abc", brand_key="abc", - expiry_option=2, - header="1", + expiry_option=enums.ExpiryOptionEnum.AFTER_SETTING_TIMES, + header=enums.HeaderEnum.ADVERTISE, footer="010-0000-0000", cdr_id="abc", copy_allowed=True, legacy=factory.LegacyInfoFactory(), ) - rcs_message.send() + assert rcs_message.send_info