From 4a0cf80dbe1a5e269bdadc8a61aedee226e1518d Mon Sep 17 00:00:00 2001 From: mini Date: Mon, 21 Jul 2025 22:16:32 +0900 Subject: [PATCH 01/34] =?UTF-8?q?chore:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(pydantic,=20sqlalchemy),=20dev=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20(pytest,=20h?= =?UTF-8?q?ttpx,=20pytest-asyncio)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 323 +++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 7 +- 2 files changed, 322 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index ff51991..1738e3e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,7 +31,7 @@ version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, @@ -92,6 +92,18 @@ d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2025.7.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, + {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -126,11 +138,11 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] -markers = "platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "distlib" @@ -182,18 +194,134 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3) testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +[[package]] +name = "greenlet" +version = "3.2.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +files = [ + {file = "greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db"}, + {file = "greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b"}, + {file = "greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712"}, + {file = "greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00"}, + {file = "greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302"}, + {file = "greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147"}, + {file = "greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5"}, + {file = "greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc"}, + {file = "greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba"}, + {file = "greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34"}, + {file = "greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688"}, + {file = "greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb"}, + {file = "greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c"}, + {file = "greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163"}, + {file = "greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849"}, + {file = "greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb"}, + {file = "greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b"}, + {file = "greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0"}, + {file = "greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36"}, + {file = "greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3"}, + {file = "greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892"}, + {file = "greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141"}, + {file = "greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a"}, + {file = "greenlet-3.2.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:42efc522c0bd75ffa11a71e09cd8a399d83fafe36db250a87cf1dacfaa15dc64"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d760f9bdfe79bff803bad32b4d8ffb2c1d2ce906313fc10a83976ffb73d64ca7"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8324319cbd7b35b97990090808fdc99c27fe5338f87db50514959f8059999805"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8c37ef5b3787567d322331d5250e44e42b58c8c713859b8a04c6065f27efbf72"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce539fb52fb774d0802175d37fcff5c723e2c7d249c65916257f0a940cee8904"}, + {file = "greenlet-3.2.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003c930e0e074db83559edc8705f3a2d066d4aa8c2f198aff1e454946efd0f26"}, + {file = "greenlet-3.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7e70ea4384b81ef9e84192e8a77fb87573138aa5d4feee541d8014e452b434da"}, + {file = "greenlet-3.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:22eb5ba839c4b2156f18f76768233fe44b23a31decd9cc0d4cc8141c211fd1b4"}, + {file = "greenlet-3.2.3-cp39-cp39-win32.whl", hash = "sha256:4532f0d25df67f896d137431b13f4cdce89f7e3d4a96387a41290910df4d3a57"}, + {file = "greenlet-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322"}, + {file = "greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "h11" version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "identify" version = "2.6.12" @@ -215,7 +343,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -224,6 +352,18 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + [[package]] name = "macholib" version = "1.16.3" @@ -318,6 +458,22 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.14.1)"] +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + [[package]] name = "pre-commit" version = "4.2.0" @@ -471,6 +627,21 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyinstaller" version = "6.14.2" @@ -524,6 +695,47 @@ files = [ packaging = ">=22.0" setuptools = ">=42.0.0" +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.1.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"}, + {file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pywin32-ctypes" version = "0.2.3" @@ -656,12 +868,108 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.41" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "SQLAlchemy-2.0.41-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win32.whl", hash = "sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2"}, + {file = "SQLAlchemy-2.0.41-cp37-cp37m-win_amd64.whl", hash = "sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda"}, + {file = "sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8"}, + {file = "sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6"}, + {file = "sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f"}, + {file = "sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win32.whl", hash = "sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45"}, + {file = "sqlalchemy-2.0.41-cp38-cp38-win_amd64.whl", hash = "sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440"}, + {file = "sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71"}, + {file = "sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576"}, + {file = "sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9"}, +] + +[package.dependencies] +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "starlette" version = "0.46.2" @@ -686,11 +994,12 @@ version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] +markers = {dev = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -750,4 +1059,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "3b5b7454d461acc6cfb10bd75966c73b99fada86f142d8aa3b6b1d16463c6c30" +content-hash = "b2b900ec980a2f48b1909700bd7c43c46ba2242bb5dfddf3acb5bc45e8244c65" diff --git a/pyproject.toml b/pyproject.toml index e13ea88..f44d187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,9 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "fastapi (>=0.115.14,<0.116.0)", - "uvicorn (>=0.35.0,<0.36.0)" + "uvicorn (>=0.35.0,<0.36.0)", + "pydantic (>=2.11.7,<3.0.0)", + "sqlalchemy (>=2.0.41,<3.0.0)" ] @@ -23,6 +25,9 @@ ruff = "^0.12.2" black = "^25.1.0" pre-commit = "^4.2.0" pyinstaller = {version = "^6.14.2", python = ">=3.11,<3.14"} +pytest = "^8.4.1" +httpx = "^0.28.1" +pytest-asyncio = "^1.1.0" # ---------------------------- From 27a9b99c91a5522d21a80bad0a90ca95daab63d9 Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 23 Jul 2025 01:11:54 +0900 Subject: [PATCH 02/34] =?UTF-8?q?feat:=20Python=20import=EB=A1=9C=20DB=20?= =?UTF-8?q?=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connections.py | 11 +++++++++ app/main.py | 19 ++++++++-------- app/schemas/db_driver_info.py | 11 +++++++++ app/services/driver_checker.py | 41 ++++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 app/api/connections.py create mode 100644 app/schemas/db_driver_info.py create mode 100644 app/services/driver_checker.py diff --git a/app/api/connections.py b/app/api/connections.py new file mode 100644 index 0000000..233bf45 --- /dev/null +++ b/app/api/connections.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +from app.schemas.db_driver_info import DBDriverInfo +from app.services.driver_checker import check_driver + +router = APIRouter() + + +@router.get("/connections/drivers/{driverId}", response_model=DBDriverInfo) +def get_driver_info(driverId: str): + return check_driver(driverId) diff --git a/app/main.py b/app/main.py index a8e0b41..c20f561 100644 --- a/app/main.py +++ b/app/main.py @@ -3,16 +3,13 @@ import uvicorn from fastapi import FastAPI -from app.api import health # 헬스 체크 -from app.core.port import get_available_port # 동적 포트 할당 -from app.api.api_router import api_router - - -from app.core.exceptions import ( - APIException, - api_exception_handler, - generic_exception_handler +from app.api import ( + connections, + health, # 헬스 체크 ) +from app.api.api_router import api_router +from app.core.exceptions import APIException, api_exception_handler, generic_exception_handler +from app.core.port import get_available_port # 동적 포트 할당 app = FastAPI() @@ -20,10 +17,14 @@ app.add_exception_handler(APIException, api_exception_handler) app.add_exception_handler(Exception, generic_exception_handler) +# 드라이버 확인 라우터 +app.include_router(connections.router) + # 라우터 app.include_router(health.router) app.include_router(api_router, prefix="/api") + @app.get("/") async def read_root(): return {"message": "Hello, FastAPI Backend!"} diff --git a/app/schemas/db_driver_info.py b/app/schemas/db_driver_info.py new file mode 100644 index 0000000..408d322 --- /dev/null +++ b/app/schemas/db_driver_info.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + + +class DBDriverInfo(BaseModel): + db_type: str + is_installed: bool + message: str + driver_path: str | None = None + driver_name: str | None = None + driver_size_bytes: int | None = None + driver_version: str | None = None diff --git a/app/services/driver_checker.py b/app/services/driver_checker.py new file mode 100644 index 0000000..1356a22 --- /dev/null +++ b/app/services/driver_checker.py @@ -0,0 +1,41 @@ +import importlib.util +import os + +from app.schemas.db_driver_info import DBDriverInfo + +DRIVER_MAP = { + "postgresql": "psycopg2", + "mysql": "pymysql", + "sqlite": "sqlite3", + "oracle": "cx_Oracle", + "sqlserver": "pyodbc", +} + + +def check_driver(driver_id: str) -> DBDriverInfo: + module_name = DRIVER_MAP.get(driver_id.lower()) + if not module_name: + return DBDriverInfo( + db_type=driver_id, + is_installed=False, + message="지원되지 않는 DB 타입입니다.", + driver_path=None, + driver_name=None, + driver_size_bytes=None, + driver_version=None, + ) + + # 해당 모듈이 현재 파이썬 환경에 설치되어 있는지 확인(즉, import 가능한지) + spec = importlib.util.find_spec(module_name) + # 설치 유무 확인 + is_installed = spec is not None + + return DBDriverInfo( + db_type=driver_id, + is_installed=is_installed, + message="드라이버가 설치되어 있습니다." if is_installed else "드라이버가 설치되어 있지 않습니다.", + driver_path=spec.origin if is_installed else None, + driver_name=os.path.basename(spec.origin) if is_installed else None, + driver_size_bytes=os.path.getsize(spec.origin) if is_installed else None, + driver_version="N/A", # 버전 확인 로직은 각 패키지별로 다르게 처리 + ) From ad8d13851369dd87f2221afeb587e59bcbd93481 Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 23 Jul 2025 01:12:54 +0900 Subject: [PATCH 03/34] =?UTF-8?q?test:=20DB=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=20=EC=84=A4=EC=B9=98=20=EC=83=81=ED=83=9C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EB=B0=8F=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tests/api/test_connections.py | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 app/tests/api/test_connections.py diff --git a/app/tests/api/test_connections.py b/app/tests/api/test_connections.py new file mode 100644 index 0000000..a742fe6 --- /dev/null +++ b/app/tests/api/test_connections.py @@ -0,0 +1,60 @@ +from unittest.mock import MagicMock, patch + +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_check_mysql_driver_installed(): + # 드라이버 설치 되어있을 경우 + mock_spec = MagicMock() + mock_spec.origin = "/usr/lib/python3.11/site-packages/mysql_driver.so" + + with patch("importlib.util.find_spec", return_value=mock_spec): + with patch("os.path.getsize", return_value=123456): + response = client.get("/connections/drivers/mysql") + assert response.status_code == 200 + data = response.json() + print(data) + assert data["db_type"] == "mysql" + assert data["is_installed"] is True + assert data["driver_path"] == mock_spec.origin + assert data["driver_name"] == "mysql_driver.so" + assert data["driver_size_bytes"] == 123456 + + +def test_check_mysql_driver_not_installed(): + # 드라이버 설치 안되어있을 경우 + with patch("importlib.util.find_spec", return_value=None): + response = client.get("/connections/drivers/mysql") + assert response.status_code == 200 + data = response.json() + print(data) + assert data["db_type"] == "mysql" + assert data["is_installed"] is False + assert data["driver_path"] is None + assert data["driver_name"] is None + assert data["driver_size_bytes"] is None + assert "설치되어 있지 않습니다" in data["message"] + + +from app.services.driver_checker import check_driver + + +def test_check_driver_unsupported_db_direct(): + # 지원되지 않는 DB 타입을 넣었을 때 + unsupported_db = "unknown_db" + response = check_driver(unsupported_db) + + # response는 DBDriverInfo 객체라 바로 속성 접근 + print(response) + + assert response.db_type == unsupported_db + assert response.is_installed is False + assert response.message == "지원되지 않는 DB 타입입니다." + assert response.driver_path is None + assert response.driver_name is None + assert response.driver_size_bytes is None + assert response.driver_version is None From aa32a8d0fcfd92e6970c8e3ea53980d40624153a Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 23 Jul 2025 02:12:59 +0900 Subject: [PATCH 04/34] =?UTF-8?q?feat:=20Python=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connections.py | 7 ++++++- app/services/driver_checker.py | 36 +++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/app/api/connections.py b/app/api/connections.py index 233bf45..470f5ee 100644 --- a/app/api/connections.py +++ b/app/api/connections.py @@ -1,7 +1,7 @@ from fastapi import APIRouter from app.schemas.db_driver_info import DBDriverInfo -from app.services.driver_checker import check_driver +from app.services.driver_checker import check_driver, is_python_environment router = APIRouter() @@ -9,3 +9,8 @@ @router.get("/connections/drivers/{driverId}", response_model=DBDriverInfo) def get_driver_info(driverId: str): return check_driver(driverId) + + +@router.get("/environment/python") +def check_python_environment(): + return {"is_python_environment": is_python_environment()} diff --git a/app/services/driver_checker.py b/app/services/driver_checker.py index 1356a22..24728c1 100644 --- a/app/services/driver_checker.py +++ b/app/services/driver_checker.py @@ -1,5 +1,7 @@ import importlib.util import os +import shutil +import sys from app.schemas.db_driver_info import DBDriverInfo @@ -12,7 +14,24 @@ } +def is_python_environment() -> bool: + """현재 환경이 Python 실행 환경인지 확인""" + # 1) sys.executable 존재 여부 확인 (현재 파이썬 인터프리터 경로) + if sys.executable: + return True + # 2) 시스템 경로에 python3 또는 python 명령어 존재 여부 확인 + if shutil.which("python3") or shutil.which("python"): + return True + return False + + def check_driver(driver_id: str) -> DBDriverInfo: + """ + 주어진 DB 드라이버 ID에 대해 + - 현재 Python 환경에서 설치 여부 확인 + - 설치되어 있으면 드라이버 정보 반환 + - 설치 안되어 있거나 미지원 타입일 경우 상태 반환 + """ module_name = DRIVER_MAP.get(driver_id.lower()) if not module_name: return DBDriverInfo( @@ -25,9 +44,20 @@ def check_driver(driver_id: str) -> DBDriverInfo: driver_version=None, ) - # 해당 모듈이 현재 파이썬 환경에 설치되어 있는지 확인(즉, import 가능한지) + # Python 환경이 아니면 설치 여부 확인 불가 (필요하면 OS 레벨 체크로 확장 가능) + if not is_python_environment(): + return DBDriverInfo( + db_type=driver_id, + is_installed=False, + message="Python 환경이 아니어서 설치 여부를 확인할 수 없습니다.", + driver_path=None, + driver_name=None, + driver_size_bytes=None, + driver_version=None, + ) + + # import 가능한지 확인해서 설치 여부 판단 spec = importlib.util.find_spec(module_name) - # 설치 유무 확인 is_installed = spec is not None return DBDriverInfo( @@ -37,5 +67,5 @@ def check_driver(driver_id: str) -> DBDriverInfo: driver_path=spec.origin if is_installed else None, driver_name=os.path.basename(spec.origin) if is_installed else None, driver_size_bytes=os.path.getsize(spec.origin) if is_installed else None, - driver_version="N/A", # 버전 확인 로직은 각 패키지별로 다르게 처리 + driver_version="N/A", # 추후 각 드라이버별 버전 확인 로직 추가 가능 ) From bede728fe2982b150614b65bde740f6cb62cb9cc Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 23 Jul 2025 02:17:40 +0900 Subject: [PATCH 05/34] =?UTF-8?q?test:=20Python=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EC=A1=B4=EC=9E=AC=20=EC=97=AC=EB=B6=80=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tests/api/test_connections.py | 47 ++++++++++++++++++------------- app/tests/api/test_environment.py | 15 ++++++++++ 2 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 app/tests/api/test_environment.py diff --git a/app/tests/api/test_connections.py b/app/tests/api/test_connections.py index a742fe6..3786730 100644 --- a/app/tests/api/test_connections.py +++ b/app/tests/api/test_connections.py @@ -3,52 +3,61 @@ from fastapi.testclient import TestClient from app.main import app +from app.services.driver_checker import check_driver client = TestClient(app) def test_check_mysql_driver_installed(): - # 드라이버 설치 되어있을 경우 mock_spec = MagicMock() mock_spec.origin = "/usr/lib/python3.11/site-packages/mysql_driver.so" - with patch("importlib.util.find_spec", return_value=mock_spec): - with patch("os.path.getsize", return_value=123456): + with patch("app.services.driver_checker.is_python_environment", return_value=True): + with patch("importlib.util.find_spec", return_value=mock_spec): + with patch("os.path.getsize", return_value=123456): + response = client.get("/connections/drivers/mysql") + assert response.status_code == 200 + data = response.json() + print(data) + assert data["db_type"] == "mysql" + assert data["is_installed"] is True + assert data["driver_path"] == mock_spec.origin + assert data["driver_name"] == "mysql_driver.so" + assert data["driver_size_bytes"] == 123456 + + +def test_check_mysql_driver_not_installed_python_env(): + # Python 환경이지만 드라이버가 설치되어 있지 않은 경우 + with patch("app.services.driver_checker.is_python_environment", return_value=True): + with patch("importlib.util.find_spec", return_value=None): response = client.get("/connections/drivers/mysql") assert response.status_code == 200 data = response.json() print(data) assert data["db_type"] == "mysql" - assert data["is_installed"] is True - assert data["driver_path"] == mock_spec.origin - assert data["driver_name"] == "mysql_driver.so" - assert data["driver_size_bytes"] == 123456 + assert data["is_installed"] is False + assert data["driver_path"] is None + assert data["driver_name"] is None + assert data["driver_size_bytes"] is None + assert "설치되어 있지 않습니다" in data["message"] -def test_check_mysql_driver_not_installed(): - # 드라이버 설치 안되어있을 경우 - with patch("importlib.util.find_spec", return_value=None): +def test_check_driver_not_python_environment(): + # Python 환경이 아닌 경우 + with patch("app.services.driver_checker.is_python_environment", return_value=False): response = client.get("/connections/drivers/mysql") assert response.status_code == 200 data = response.json() print(data) assert data["db_type"] == "mysql" assert data["is_installed"] is False - assert data["driver_path"] is None - assert data["driver_name"] is None - assert data["driver_size_bytes"] is None - assert "설치되어 있지 않습니다" in data["message"] - - -from app.services.driver_checker import check_driver + assert "Python 환경이 아니어서" in data["message"] def test_check_driver_unsupported_db_direct(): - # 지원되지 않는 DB 타입을 넣었을 때 unsupported_db = "unknown_db" response = check_driver(unsupported_db) - # response는 DBDriverInfo 객체라 바로 속성 접근 print(response) assert response.db_type == unsupported_db diff --git a/app/tests/api/test_environment.py b/app/tests/api/test_environment.py new file mode 100644 index 0000000..da9a454 --- /dev/null +++ b/app/tests/api/test_environment.py @@ -0,0 +1,15 @@ +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_check_python_environment_api(): + response = client.get("/environment/python") + assert response.status_code == 200 + data = response.json() + print("[python env result]", data) + + assert "is_python_environment" in data + assert isinstance(data["is_python_environment"], bool) From 57c109286470c5c616ce050b94b12ea83d5fde12 Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 23 Jul 2025 03:09:23 +0900 Subject: [PATCH 06/34] =?UTF-8?q?feat:=20macOS=20=EB=B0=8F=20Windows=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=20OS=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=20=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/db_driver_info.py | 2 + app/services/driver_checker.py | 135 +++++++++++++++++++++++++----- app/tests/api/test_connections.py | 40 +++++++-- 3 files changed, 148 insertions(+), 29 deletions(-) diff --git a/app/schemas/db_driver_info.py b/app/schemas/db_driver_info.py index 408d322..f79412b 100644 --- a/app/schemas/db_driver_info.py +++ b/app/schemas/db_driver_info.py @@ -9,3 +9,5 @@ class DBDriverInfo(BaseModel): driver_name: str | None = None driver_size_bytes: int | None = None driver_version: str | None = None + os_name: str | None = None + os_full_name: str | None = None diff --git a/app/services/driver_checker.py b/app/services/driver_checker.py index 24728c1..4d6cb86 100644 --- a/app/services/driver_checker.py +++ b/app/services/driver_checker.py @@ -1,10 +1,21 @@ +import importlib import importlib.util +import logging import os +import platform import shutil +import subprocess import sys from app.schemas.db_driver_info import DBDriverInfo +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +os_simple_name = platform.system() # Darwin, Windows, Linux 등 간단 이름 +os_full_name = platform.platform() # Darwin-22.5.0-x86_64, Windows-10-10.0.19045 등 상세 버전 포함 + +# 주요 DB별 대표적인 파이썬 드라이버들 복수 리스트로 작성 DRIVER_MAP = { "postgresql": "psycopg2", "mysql": "pymysql", @@ -14,26 +25,67 @@ } +# OS 레벨에서 드라이버 설치 확인용 명령어 (간단 체크용) +OS_DRIVER_CHECK_COMMANDS = { + "postgresql": { + "macos": ["which", "psql"], + "windows": ["where", "psql"], + }, + "mysql": { + "macos": ["which", "mysql"], + "windows": ["where", "mysql"], + }, + "sqlite": { + "macos": ["which", "sqlite3"], + "windows": ["where", "sqlite3"], + }, + "oracle": { + "macos": ["which", "sqlplus"], + "windows": ["where", "sqlplus"], + }, + "sqlserver": { + "macos": ["which", "sqlcmd"], + "windows": ["where", "sqlcmd"], + }, +} + + def is_python_environment() -> bool: """현재 환경이 Python 실행 환경인지 확인""" - # 1) sys.executable 존재 여부 확인 (현재 파이썬 인터프리터 경로) if sys.executable: return True - # 2) 시스템 경로에 python3 또는 python 명령어 존재 여부 확인 if shutil.which("python3") or shutil.which("python"): return True return False +def check_os_driver_installed(driver_id: str) -> bool: + """Python 환경이 아닐 때 OS 레벨에서 DB 클라이언트 도구 설치 여부 확인""" + system = platform.system().lower() + driver_key = driver_id.lower() + + if driver_key not in OS_DRIVER_CHECK_COMMANDS: + return False # 지원하지 않는 드라이버 + + if system == "darwin": + cmd = OS_DRIVER_CHECK_COMMANDS[driver_key]["macos"] + elif system == "windows": + cmd = OS_DRIVER_CHECK_COMMANDS[driver_key]["windows"] + else: + return False # Linux 등은 필요 시 추가 + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return bool(result.stdout.strip()) + except Exception: + return False + + def check_driver(driver_id: str) -> DBDriverInfo: - """ - 주어진 DB 드라이버 ID에 대해 - - 현재 Python 환경에서 설치 여부 확인 - - 설치되어 있으면 드라이버 정보 반환 - - 설치 안되어 있거나 미지원 타입일 경우 상태 반환 - """ - module_name = DRIVER_MAP.get(driver_id.lower()) - if not module_name: + driver_key = driver_id.lower() + module_names = DRIVER_MAP.get(driver_key) + + if not module_names: return DBDriverInfo( db_type=driver_id, is_installed=False, @@ -42,30 +94,73 @@ def check_driver(driver_id: str) -> DBDriverInfo: driver_name=None, driver_size_bytes=None, driver_version=None, + os_name=os_simple_name, + os_full_name=os_full_name, ) - # Python 환경이 아니면 설치 여부 확인 불가 (필요하면 OS 레벨 체크로 확장 가능) + logger.info(f"[check_driver] 요청된 드라이버: {driver_id} → 모듈명: {module_names}") + logger.info(f"[check_driver] Python 환경 여부: {is_python_environment()}") + if not is_python_environment(): + os_installed = check_os_driver_installed(driver_key) return DBDriverInfo( db_type=driver_id, - is_installed=False, - message="Python 환경이 아니어서 설치 여부를 확인할 수 없습니다.", + is_installed=os_installed, + message=( + "Python 환경이 아니어서 OS 레벨로 설치 여부를 확인했습니다." + if os_installed + else "Python 환경이 아니며 OS 레벨에서도 드라이버를 찾을 수 없습니다." + ), driver_path=None, driver_name=None, driver_size_bytes=None, driver_version=None, + os_name=os_simple_name, + os_full_name=os_full_name, ) - # import 가능한지 확인해서 설치 여부 판단 - spec = importlib.util.find_spec(module_name) - is_installed = spec is not None + # module_names가 리스트일 때 첫 발견된 드라이버를 찾음 + if isinstance(module_names, str): + module_names = [module_names] + + installed_module = None + spec = None + for mod_name in module_names: + spec = importlib.util.find_spec(mod_name) + if spec is not None: + installed_module = mod_name + break + + is_installed = installed_module is not None + logger.info(f"[check_driver] 발견된 모듈명: {installed_module}, spec: {spec}") + + driver_path = None + driver_name = None + driver_size_bytes = None + driver_version = "N/A" + + if is_installed and spec and spec.origin: + driver_path = spec.origin + driver_name = os.path.basename(driver_path) + try: + driver_size_bytes = os.path.getsize(driver_path) + except Exception: + driver_size_bytes = None + + try: + mod = importlib.import_module(installed_module) + driver_version = getattr(mod, "__version__", "Unknown") + except Exception: + driver_version = "Unknown" return DBDriverInfo( db_type=driver_id, is_installed=is_installed, message="드라이버가 설치되어 있습니다." if is_installed else "드라이버가 설치되어 있지 않습니다.", - driver_path=spec.origin if is_installed else None, - driver_name=os.path.basename(spec.origin) if is_installed else None, - driver_size_bytes=os.path.getsize(spec.origin) if is_installed else None, - driver_version="N/A", # 추후 각 드라이버별 버전 확인 로직 추가 가능 + driver_path=driver_path, + driver_name=driver_name, + driver_size_bytes=driver_size_bytes, + driver_version=driver_version, + os_name=os_simple_name, + os_full_name=os_full_name, ) diff --git a/app/tests/api/test_connections.py b/app/tests/api/test_connections.py index 3786730..f011202 100644 --- a/app/tests/api/test_connections.py +++ b/app/tests/api/test_connections.py @@ -1,3 +1,5 @@ +import subprocess +import sys from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient @@ -5,6 +7,7 @@ from app.main import app from app.services.driver_checker import check_driver +print(f"Python 실행 경로: {sys.executable}") client = TestClient(app) @@ -42,16 +45,35 @@ def test_check_mysql_driver_not_installed_python_env(): assert "설치되어 있지 않습니다" in data["message"] -def test_check_driver_not_python_environment(): - # Python 환경이 아닌 경우 +def test_check_driver_not_python_environment_driver_installed(): + # Python 환경이 아니고, OS 레벨에서 설치되어 있다고 판단되는 경우 with patch("app.services.driver_checker.is_python_environment", return_value=False): - response = client.get("/connections/drivers/mysql") - assert response.status_code == 200 - data = response.json() - print(data) - assert data["db_type"] == "mysql" - assert data["is_installed"] is False - assert "Python 환경이 아니어서" in data["message"] + # subprocess.run 결과를 mocking: 설치되어 있다고 판단 + mock_subproc_result = subprocess.CompletedProcess( + args=["which", "mysql"], returncode=0, stdout=b"/usr/bin/mysql\n" + ) + with patch("subprocess.run", return_value=mock_subproc_result): + response = client.get("/connections/drivers/mysql") + assert response.status_code == 200 + data = response.json() + print(data) + assert data["db_type"] == "mysql" + assert data["is_installed"] is True + assert "Python 환경이 아니어서 OS 레벨로 설치 여부를 확인했습니다." in data["message"] + + +def test_check_driver_not_python_environment_driver_not_installed(): + # Python 환경이 아니고, OS 레벨에서 설치 안되어 있다고 판단되는 경우 + with patch("app.services.driver_checker.is_python_environment", return_value=False): + mock_subproc_result = subprocess.CompletedProcess(args=["which", "mysql"], returncode=1, stdout=b"") + with patch("subprocess.run", return_value=mock_subproc_result): + response = client.get("/connections/drivers/mysql") + assert response.status_code == 200 + data = response.json() + print(data) + assert data["db_type"] == "mysql" + assert data["is_installed"] is False + assert "Python 환경이 아니며 OS 레벨에서도 드라이버를 찾을 수 없습니다." in data["message"] def test_check_driver_unsupported_db_direct(): From e8694a52da492d428470c6abe62f150feaf30736 Mon Sep 17 00:00:00 2001 From: mini Date: Wed, 23 Jul 2025 03:25:35 +0900 Subject: [PATCH 07/34] =?UTF-8?q?fix:=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=EB=AA=85=EC=9D=B4=20=EB=8B=A8=EC=9D=BC=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=EC=9D=BC=20=EB=95=8C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=E2=80=94=20=EB=AA=A8=EB=93=A0=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=EB=AA=85=EC=9D=84=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/driver_checker.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/services/driver_checker.py b/app/services/driver_checker.py index 4d6cb86..c6aa117 100644 --- a/app/services/driver_checker.py +++ b/app/services/driver_checker.py @@ -17,11 +17,12 @@ # 주요 DB별 대표적인 파이썬 드라이버들 복수 리스트로 작성 DRIVER_MAP = { - "postgresql": "psycopg2", - "mysql": "pymysql", - "sqlite": "sqlite3", - "oracle": "cx_Oracle", - "sqlserver": "pyodbc", + "postgresql": ["psycopg2", "psycopg2_binary", "pg8000"], + "mysql": ["mysql.connector", "pymysql", "MySQLdb", "oursql"], + "sqlite": ["sqlite3"], + "oracle": ["cx_Oracle", "oracledb"], + "sqlserver": ["pyodbc", "pymssql"], + "mariadb": ["mariadb", "mysql.connector", "pymysql", "MySQLdb", "oursql"], } @@ -124,12 +125,17 @@ def check_driver(driver_id: str) -> DBDriverInfo: module_names = [module_names] installed_module = None + spec = None + for mod_name in module_names: - spec = importlib.util.find_spec(mod_name) - if spec is not None: + try: + mod = importlib.import_module(mod_name) installed_module = mod_name + spec = getattr(mod, "__spec__", None) break + except ModuleNotFoundError: + continue is_installed = installed_module is not None logger.info(f"[check_driver] 발견된 모듈명: {installed_module}, spec: {spec}") From 59256f90b40ff50e552ec29acc69bc87025b7601 Mon Sep 17 00:00:00 2001 From: mini Date: Thu, 24 Jul 2025 22:27:14 +0900 Subject: [PATCH 08/34] =?UTF-8?q?test:=20OS=20=EB=A0=88=EB=B2=A8=20?= =?UTF-8?q?=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/driver_checker.py | 3 + ...st_connections.py => test_driver_check.py} | 57 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) rename app/tests/api/{test_connections.py => test_driver_check.py} (65%) diff --git a/app/services/driver_checker.py b/app/services/driver_checker.py index c6aa117..90207b6 100644 --- a/app/services/driver_checker.py +++ b/app/services/driver_checker.py @@ -145,6 +145,9 @@ def check_driver(driver_id: str) -> DBDriverInfo: driver_size_bytes = None driver_version = "N/A" + # print("DEBUG: spec=", spec) + # print("DEBUG: is_installed=", is_installed) + if is_installed and spec and spec.origin: driver_path = spec.origin driver_name = os.path.basename(driver_path) diff --git a/app/tests/api/test_connections.py b/app/tests/api/test_driver_check.py similarity index 65% rename from app/tests/api/test_connections.py rename to app/tests/api/test_driver_check.py index f011202..1620ed6 100644 --- a/app/tests/api/test_connections.py +++ b/app/tests/api/test_driver_check.py @@ -1,3 +1,4 @@ +import json import subprocess import sys from unittest.mock import MagicMock, patch @@ -11,32 +12,43 @@ client = TestClient(app) -def test_check_mysql_driver_installed(): - mock_spec = MagicMock() - mock_spec.origin = "/usr/lib/python3.11/site-packages/mysql_driver.so" +@patch("app.services.driver_checker.importlib.import_module") +@patch("os.path.getsize", return_value=123456) +@patch("app.services.driver_checker.is_python_environment", return_value=True) +def test_check_mysql_driver_installed(mock_env, mock_getsize, mock_import_module): + # ✅ 모듈과 spec 모킹 + spec_mock = MagicMock() + spec_mock.origin = "/usr/lib/python3.11/site-packages/mysql_driver.so" - with patch("app.services.driver_checker.is_python_environment", return_value=True): - with patch("importlib.util.find_spec", return_value=mock_spec): - with patch("os.path.getsize", return_value=123456): - response = client.get("/connections/drivers/mysql") - assert response.status_code == 200 - data = response.json() - print(data) - assert data["db_type"] == "mysql" - assert data["is_installed"] is True - assert data["driver_path"] == mock_spec.origin - assert data["driver_name"] == "mysql_driver.so" - assert data["driver_size_bytes"] == 123456 + mock_module = MagicMock() + mock_module.__spec__ = spec_mock + mock_module.__version__ = "8.0.0" + mock_import_module.return_value = mock_module + + from app.main import app + + client = TestClient(app) + response = client.get("/connections/drivers/mysql") + + data = response.json() + print(json.dumps(data, indent=4, ensure_ascii=False)) + + assert data["is_installed"] is True + assert data["driver_path"] == spec_mock.origin + assert data["driver_size_bytes"] == 123456 + assert data["driver_version"] == "8.0.0" + assert data["driver_name"] == "mysql_driver.so" def test_check_mysql_driver_not_installed_python_env(): - # Python 환경이지만 드라이버가 설치되어 있지 않은 경우 + mock_spec = None # 드라이버 미설치 상황에서는 None + with patch("app.services.driver_checker.is_python_environment", return_value=True): - with patch("importlib.util.find_spec", return_value=None): + with patch("app.services.driver_checker.importlib.util.find_spec", return_value=mock_spec): response = client.get("/connections/drivers/mysql") assert response.status_code == 200 data = response.json() - print(data) + print(json.dumps(data, indent=4, ensure_ascii=False)) assert data["db_type"] == "mysql" assert data["is_installed"] is False assert data["driver_path"] is None @@ -46,9 +58,7 @@ def test_check_mysql_driver_not_installed_python_env(): def test_check_driver_not_python_environment_driver_installed(): - # Python 환경이 아니고, OS 레벨에서 설치되어 있다고 판단되는 경우 with patch("app.services.driver_checker.is_python_environment", return_value=False): - # subprocess.run 결과를 mocking: 설치되어 있다고 판단 mock_subproc_result = subprocess.CompletedProcess( args=["which", "mysql"], returncode=0, stdout=b"/usr/bin/mysql\n" ) @@ -56,21 +66,20 @@ def test_check_driver_not_python_environment_driver_installed(): response = client.get("/connections/drivers/mysql") assert response.status_code == 200 data = response.json() - print(data) + print(json.dumps(data, indent=4, ensure_ascii=False)) assert data["db_type"] == "mysql" assert data["is_installed"] is True assert "Python 환경이 아니어서 OS 레벨로 설치 여부를 확인했습니다." in data["message"] def test_check_driver_not_python_environment_driver_not_installed(): - # Python 환경이 아니고, OS 레벨에서 설치 안되어 있다고 판단되는 경우 with patch("app.services.driver_checker.is_python_environment", return_value=False): mock_subproc_result = subprocess.CompletedProcess(args=["which", "mysql"], returncode=1, stdout=b"") with patch("subprocess.run", return_value=mock_subproc_result): response = client.get("/connections/drivers/mysql") assert response.status_code == 200 data = response.json() - print(data) + print(json.dumps(data, indent=4, ensure_ascii=False)) assert data["db_type"] == "mysql" assert data["is_installed"] is False assert "Python 환경이 아니며 OS 레벨에서도 드라이버를 찾을 수 없습니다." in data["message"] @@ -80,7 +89,7 @@ def test_check_driver_unsupported_db_direct(): unsupported_db = "unknown_db" response = check_driver(unsupported_db) - print(response) + print(json.dumps(response.model_dump(), indent=4, ensure_ascii=False)) assert response.db_type == unsupported_db assert response.is_installed is False From 929b56753722de8f17200a9c74fca7c2de0f2d09 Mon Sep 17 00:00:00 2001 From: mini Date: Fri, 25 Jul 2025 00:35:20 +0900 Subject: [PATCH 09/34] =?UTF-8?q?chore:=20DB=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(PostgreSQL,=20MySQL,=20Oracle=20=EB=93=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 212 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 7 +- 2 files changed, 217 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1738e3e..07b59ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,6 +144,32 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +[[package]] +name = "cx-oracle" +version = "8.3.0" +description = "Python interface to Oracle" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "cx_Oracle-8.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b6a23da225f03f50a81980c61dbd6a358c3575f212ca7f4c22bb65a9faf94f7f"}, + {file = "cx_Oracle-8.3.0-cp310-cp310-win32.whl", hash = "sha256:715a8bbda5982af484ded14d184304cc552c1096c82471dd2948298470e88a04"}, + {file = "cx_Oracle-8.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:07f01608dfb6603a8f2a868fc7c7bdc951480f187df8dbc50f4d48c884874e6a"}, + {file = "cx_Oracle-8.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4b3afe7a911cebaceda908228d36839f6441cbd38e5df491ec25960562bb01a0"}, + {file = "cx_Oracle-8.3.0-cp36-cp36m-win32.whl", hash = "sha256:076ffb71279d6b2dcbf7df028f62a01e18ce5bb73d8b01eab582bf14a62f4a61"}, + {file = "cx_Oracle-8.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b82e4b165ffd807a2bd256259a6b81b0a2452883d39f987509e2292d494ea163"}, + {file = "cx_Oracle-8.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b902db61dcdcbbf8dd981f5a46d72fef40c5150c7fc0eb0f0698b462d6eb834e"}, + {file = "cx_Oracle-8.3.0-cp37-cp37m-win32.whl", hash = "sha256:4c82ca74442c298ceec56d207450c192e06ecf8ad52eb4aaad0812e147ceabf7"}, + {file = "cx_Oracle-8.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:54164974d526b76fdefb0b66a42b68e1fca5df78713d0eeb8c1d0047b83f6bcf"}, + {file = "cx_Oracle-8.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:410747d542e5f94727f5f0e42e9706c772cf9094fb348ce965ab88b3a9e4d2d8"}, + {file = "cx_Oracle-8.3.0-cp38-cp38-win32.whl", hash = "sha256:3baa878597c5fadb2c72f359f548431c7be001e722ce4a4ebdf3d2293a1bb70b"}, + {file = "cx_Oracle-8.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:de42bdc882abdc5cea54597da27a05593b44143728e5b629ad5d35decb1a2036"}, + {file = "cx_Oracle-8.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:df412238a9948340591beee9ec64fa62a2efacc0d91107034a7023e2991fba97"}, + {file = "cx_Oracle-8.3.0-cp39-cp39-win32.whl", hash = "sha256:70d3cf030aefd71f99b45beba77237b2af448adf5e26be0db3d0d3dee6ea4230"}, + {file = "cx_Oracle-8.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf01ce87edb4ef663b2e5bd604e1e0154d2cc2f12b60301f788b569d9db8a900"}, + {file = "cx_Oracle-8.3.0.tar.gz", hash = "sha256:3b2d215af4441463c97ea469b9cc307460739f89fdfa8ea222ea3518f1a424d9"}, +] + [[package]] name = "distlib" version = "0.3.9" @@ -392,6 +418,49 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "mysql-connector-python" +version = "9.4.0" +description = "A self-contained Python driver for communicating with MySQL servers, using an API that is compliant with the Python Database API Specification v2.0 (PEP 249)." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "mysql_connector_python-9.4.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3c2603e00516cf4208c6266e85c5c87d5f4d0ac79768106d50de42ccc8414c05"}, + {file = "mysql_connector_python-9.4.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:47884fcb050112b8bef3458e17eac47cc81a6cbbf3524e3456146c949772d9b4"}, + {file = "mysql_connector_python-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f14b6936cd326e212fc9ab5f666dea3efea654f0cb644460334e60e22986e735"}, + {file = "mysql_connector_python-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0f5ad70355720e64b72d7c068e858c9fd1f69b671d9575f857f235a10f878939"}, + {file = "mysql_connector_python-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:7106670abce510e440d393e27fc3602b8cf21e7a8a80216cc9ad9a68cd2e4595"}, + {file = "mysql_connector_python-9.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7df1a8ddd182dd8adc914f6dc902a986787bf9599705c29aca7b2ce84e79d361"}, + {file = "mysql_connector_python-9.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3892f20472e13e63b1fb4983f454771dd29f211b09724e69a9750e299542f2f8"}, + {file = "mysql_connector_python-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d3e87142103d71c4df647ece30f98e85e826652272ed1c74822b56f6acdc38e7"}, + {file = "mysql_connector_python-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b27fcd403436fe83bafb2fe7fcb785891e821e639275c4ad3b3bd1e25f533206"}, + {file = "mysql_connector_python-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd6ff5afb9c324b0bbeae958c93156cce4168c743bf130faf224d52818d1f0ee"}, + {file = "mysql_connector_python-9.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4efa3898a24aba6a4bfdbf7c1f5023c78acca3150d72cc91199cca2ccd22f76f"}, + {file = "mysql_connector_python-9.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:665c13e7402235162e5b7a2bfdee5895192121b64ea455c90a81edac6a48ede5"}, + {file = "mysql_connector_python-9.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:815aa6cad0f351c1223ef345781a538f2e5e44ef405fdb3851eb322bd9c4ca2b"}, + {file = "mysql_connector_python-9.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b3436a2c8c0ec7052932213e8d01882e6eb069dbab33402e685409084b133a1c"}, + {file = "mysql_connector_python-9.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:57b0c224676946b70548c56798d5023f65afa1ba5b8ac9f04a143d27976c7029"}, + {file = "mysql_connector_python-9.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:fde3bbffb5270a4b02077029914e6a9d2ec08f67d8375b4111432a2778e7540b"}, + {file = "mysql_connector_python-9.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:25f77ad7d845df3b5a5a3a6a8d1fed68248dc418a6938a371d1ddaaab6b9a8e3"}, + {file = "mysql_connector_python-9.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:227dd420c71e6d4788d52d98f298e563f16b6853577e5ade4bd82d644257c812"}, + {file = "mysql_connector_python-9.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5163381a312d38122eded2197eb5cd7ccf1a5c5881d4e7a6de10d6ea314d088e"}, + {file = "mysql_connector_python-9.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:c727cb1f82b40c9aaa7a15ab5cf0a7f87c5d8dce32eab5ff2530a4aa6054e7df"}, + {file = "mysql_connector_python-9.4.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:20f8154ab5c0ed444f8ef8e5fa91e65215037db102c137b5f995ebfffd309b78"}, + {file = "mysql_connector_python-9.4.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:7b8976d89d67c8b0dc452471cb557d9998ed30601fb69a876bf1f0ecaa7954a4"}, + {file = "mysql_connector_python-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:4ee4fe1b067e243aae21981e4b9f9d300a3104814b8274033ca8fc7a89b1729e"}, + {file = "mysql_connector_python-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1c6b95404e80d003cd452e38674e91528e2b3a089fe505c882f813b564e64f9d"}, + {file = "mysql_connector_python-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8f820c111335f225d63367307456eb7e10494f87e7a94acded3bb762e55a6d4"}, + {file = "mysql_connector_python-9.4.0-py2.py3-none-any.whl", hash = "sha256:56e679169c704dab279b176fab2a9ee32d2c632a866c0f7cd48a8a1e2cf802c4"}, + {file = "mysql_connector_python-9.4.0.tar.gz", hash = "sha256:d111360332ae78933daf3d48ff497b70739aa292ab0017791a33e826234e743b"}, +] + +[package.extras] +dns-srv = ["dnspython (==2.6.1)"] +gssapi = ["gssapi (==1.8.3)"] +telemetry = ["opentelemetry-api (==1.33.1)", "opentelemetry-exporter-otlp-proto-http (==1.33.1)", "opentelemetry-sdk (==1.33.1)"] +webauthn = ["fido2 (==1.1.2)"] + [[package]] name = "nodeenv" version = "1.9.1" @@ -493,6 +562,84 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, + {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, + {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, + {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, + {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, + {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -695,6 +842,69 @@ files = [ packaging = ">=22.0" setuptools = ">=42.0.0" +[[package]] +name = "pymysql" +version = "1.1.1" +description = "Pure Python MySQL Driver" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"}, + {file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"}, +] + +[package.extras] +ed25519 = ["PyNaCl (>=1.4.0)"] +rsa = ["cryptography"] + +[[package]] +name = "pyodbc" +version = "5.2.0" +description = "DB API module for ODBC" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyodbc-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb0850e3e3782f57457feed297e220bb20c3e8fd7550d7a6b6bb96112bd9b6fe"}, + {file = "pyodbc-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0dae0fb86078c87acf135dbe5afd3c7d15d52ab0db5965c44159e84058c3e2fb"}, + {file = "pyodbc-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6493b9c7506ca964b80ad638d0dc82869df7058255d71f04fdd1405e88bcb36b"}, + {file = "pyodbc-5.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e04de873607fb960e71953c164c83e8e5d9291ce0d69e688e54947b254b04902"}, + {file = "pyodbc-5.2.0-cp310-cp310-win32.whl", hash = "sha256:74135cb10c1dcdbd99fe429c61539c232140e62939fa7c69b0a373cc552e4a08"}, + {file = "pyodbc-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:d287121eeaa562b9ab3d4c52fa77c793dfedd127049273eb882a05d3d67a8ce8"}, + {file = "pyodbc-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4627779f0a608b51ce2d2fe6d1d395384e65ca36248bf9dbb6d7cf2c8fda1cab"}, + {file = "pyodbc-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d997d3b6551273647825c734158ca8a6f682df269f6b3975f2499c01577ddec"}, + {file = "pyodbc-5.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5102007a8c78dd2fc1c1b6f6147de8cfc020f81013e4b46c33e66aaa7d1bf7b1"}, + {file = "pyodbc-5.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e3cbc7075a46c411b531ada557c4aef13d034060a70077717124cabc1717e2d"}, + {file = "pyodbc-5.2.0-cp311-cp311-win32.whl", hash = "sha256:de1ee7ec2eb326b7be5e2c4ce20d472c5ef1a6eb838d126d1d26779ff5486e49"}, + {file = "pyodbc-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:113f904b9852c12f10c7a3288f5a3563ecdbbefe3ccc829074a9eb8255edcd29"}, + {file = "pyodbc-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be43d1ece4f2cf4d430996689d89a1a15aeb3a8da8262527e5ced5aee27e89c3"}, + {file = "pyodbc-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9f7badd0055221a744d76c11440c0856fd2846ed53b6555cf8f0a8893a3e4b03"}, + {file = "pyodbc-5.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad633c52f4f4e7691daaa2278d6e6ebb2fe4ae7709e610e22c7dd1a1d620cf8b"}, + {file = "pyodbc-5.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d086a8f7a302b74c9c2e77bedf954a603b19168af900d4d3a97322e773df63"}, + {file = "pyodbc-5.2.0-cp312-cp312-win32.whl", hash = "sha256:0e4412f8e608db2a4be5bcc75f9581f386ed6a427dbcb5eac795049ba6fc205e"}, + {file = "pyodbc-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1f5686b142759c5b2bdbeaa0692622c2ebb1f10780eb3c174b85f5607fbcf55"}, + {file = "pyodbc-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:26844d780045bbc3514d5c2f0d89e7fda7df7db0bd24292eb6902046f5730885"}, + {file = "pyodbc-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26d2d8fd53b71204c755abc53b0379df4e23fd9a40faf211e1cb87e8a32470f0"}, + {file = "pyodbc-5.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a27996b6d27e275dfb5fe8a34087ba1cacadfd1439e636874ef675faea5149d9"}, + {file = "pyodbc-5.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf42c4bd323b8fd01f1cd900cca2d09232155f9b8f0b9bcd0be66763588ce64"}, + {file = "pyodbc-5.2.0-cp313-cp313-win32.whl", hash = "sha256:207f16b7e9bf09c591616429ebf2b47127e879aad21167ac15158910dc9bbcda"}, + {file = "pyodbc-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:96d3127f28c0dacf18da7ae009cd48eac532d3dcc718a334b86a3c65f6a5ef5c"}, + {file = "pyodbc-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:770e1ac2e7bdf31439bf1d57a1d34ae37d6151216367e8e3f6cdc275006c8bb0"}, + {file = "pyodbc-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fde753fcea625bfaed36edae34c2fba15bf0b5d0ea27474ee038ef47b684d1d"}, + {file = "pyodbc-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57843b9792994f9e73b91667da6452a4f2d7caaa2499598783eb972c4b6eb93"}, + {file = "pyodbc-5.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f38adc47d36af392475cd4aaae0f35652fdc9e8364bf155810fe1be591336f"}, + {file = "pyodbc-5.2.0-cp38-cp38-win32.whl", hash = "sha256:dc5342d1d09466f9e76e3979551f9205a01ff0ea78b02d2d889171e8c3c4fb9c"}, + {file = "pyodbc-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5323be83fedc79a6d1e1b96e67bdc368c1d3f1562b8f8184b735acdd749ae9"}, + {file = "pyodbc-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e8f4ee2c523bbe85124540ffad62a3b62ae481f012e390ef93e0602b6302e5e"}, + {file = "pyodbc-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:057b8ede91b21d9f0ef58210d1ca1aad704e641ca68ac6b02f109d86b61d7402"}, + {file = "pyodbc-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f0ecbc7067467df95c9b8bd38fb2682c4a13a3402d77dccaddf1e145cea8cc0"}, + {file = "pyodbc-5.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b7f8324fa01c09fe4843ad8adb0b131299ef263a1fb9e63830c9cd1d5c45e4"}, + {file = "pyodbc-5.2.0-cp39-cp39-win32.whl", hash = "sha256:600ef6f562f609f5612ffaa8a93827249150aa3030c867937c87b24a1608967e"}, + {file = "pyodbc-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:b77556349746fb90416a48bd114cd7323f7e2559a4b263dada935f9b406ba59b"}, + {file = "pyodbc-5.2.0.tar.gz", hash = "sha256:de8be39809c8ddeeee26a4b876a6463529cd487a60d1393eb2a93e9bcd44a8f5"}, +] + [[package]] name = "pytest" version = "8.4.1" @@ -1059,4 +1269,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "b2b900ec980a2f48b1909700bd7c43c46ba2242bb5dfddf3acb5bc45e8244c65" +content-hash = "b74b3112c796bc9e08554482dd7dc61da090b1788706803ec8c2b9d7668fd563" diff --git a/pyproject.toml b/pyproject.toml index f44d187..0f8cca5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,12 @@ dependencies = [ "fastapi (>=0.115.14,<0.116.0)", "uvicorn (>=0.35.0,<0.36.0)", "pydantic (>=2.11.7,<3.0.0)", - "sqlalchemy (>=2.0.41,<3.0.0)" + "sqlalchemy (>=2.0.41,<3.0.0)", + "psycopg2-binary (>=2.9.10,<3.0.0)", + "mysql-connector-python (>=9.4.0,<10.0.0)", + "pymysql (>=1.1.1,<2.0.0)", + "cx-oracle (>=8.3.0,<9.0.0)", + "pyodbc (>=5.2.0,<6.0.0)" ] From 8adb031faae8ea47988098d9d23f8df5126a1e51 Mon Sep 17 00:00:00 2001 From: mini Date: Fri, 25 Jul 2025 01:30:33 +0900 Subject: [PATCH 10/34] =?UTF-8?q?feat:=20=EC=A7=80=EC=9B=90=20DB=20?= =?UTF-8?q?=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EC=82=AC=EC=A0=84=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=EB=A1=9C=20=ED=8C=8C=EC=9D=B4=EC=8D=AC=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD,=20OS=EB=A0=88=EB=B2=A8=20=EB=93=9C=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B2=84=20=EC=B2=B4=ED=81=AC=20=EC=83=9D=EB=9E=B5=20?= =?UTF-8?q?=EB=B0=8F=20=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connections.py | 14 +-- app/schemas/db_driver_info.py | 13 -- app/services/driver_checker.py | 175 --------------------------- app/services/driver_info_provider.py | 45 +++++++ app/tests/api/test_driver_check.py | 100 --------------- app/tests/api/test_environment.py | 15 --- 6 files changed, 49 insertions(+), 313 deletions(-) delete mode 100644 app/schemas/db_driver_info.py delete mode 100644 app/services/driver_checker.py create mode 100644 app/services/driver_info_provider.py delete mode 100644 app/tests/api/test_driver_check.py delete mode 100644 app/tests/api/test_environment.py diff --git a/app/api/connections.py b/app/api/connections.py index 470f5ee..ce99b9b 100644 --- a/app/api/connections.py +++ b/app/api/connections.py @@ -1,16 +1,10 @@ from fastapi import APIRouter -from app.schemas.db_driver_info import DBDriverInfo -from app.services.driver_checker import check_driver, is_python_environment +from app.services.driver_info_provider import db_driver_info router = APIRouter() -@router.get("/connections/drivers/{driverId}", response_model=DBDriverInfo) -def get_driver_info(driverId: str): - return check_driver(driverId) - - -@router.get("/environment/python") -def check_python_environment(): - return {"is_python_environment": is_python_environment()} +@router.get("/connections/drivers/{driverId}") +def read_driver_info(driverId: str): + return db_driver_info(driverId) diff --git a/app/schemas/db_driver_info.py b/app/schemas/db_driver_info.py deleted file mode 100644 index f79412b..0000000 --- a/app/schemas/db_driver_info.py +++ /dev/null @@ -1,13 +0,0 @@ -from pydantic import BaseModel - - -class DBDriverInfo(BaseModel): - db_type: str - is_installed: bool - message: str - driver_path: str | None = None - driver_name: str | None = None - driver_size_bytes: int | None = None - driver_version: str | None = None - os_name: str | None = None - os_full_name: str | None = None diff --git a/app/services/driver_checker.py b/app/services/driver_checker.py deleted file mode 100644 index 90207b6..0000000 --- a/app/services/driver_checker.py +++ /dev/null @@ -1,175 +0,0 @@ -import importlib -import importlib.util -import logging -import os -import platform -import shutil -import subprocess -import sys - -from app.schemas.db_driver_info import DBDriverInfo - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -os_simple_name = platform.system() # Darwin, Windows, Linux 등 간단 이름 -os_full_name = platform.platform() # Darwin-22.5.0-x86_64, Windows-10-10.0.19045 등 상세 버전 포함 - -# 주요 DB별 대표적인 파이썬 드라이버들 복수 리스트로 작성 -DRIVER_MAP = { - "postgresql": ["psycopg2", "psycopg2_binary", "pg8000"], - "mysql": ["mysql.connector", "pymysql", "MySQLdb", "oursql"], - "sqlite": ["sqlite3"], - "oracle": ["cx_Oracle", "oracledb"], - "sqlserver": ["pyodbc", "pymssql"], - "mariadb": ["mariadb", "mysql.connector", "pymysql", "MySQLdb", "oursql"], -} - - -# OS 레벨에서 드라이버 설치 확인용 명령어 (간단 체크용) -OS_DRIVER_CHECK_COMMANDS = { - "postgresql": { - "macos": ["which", "psql"], - "windows": ["where", "psql"], - }, - "mysql": { - "macos": ["which", "mysql"], - "windows": ["where", "mysql"], - }, - "sqlite": { - "macos": ["which", "sqlite3"], - "windows": ["where", "sqlite3"], - }, - "oracle": { - "macos": ["which", "sqlplus"], - "windows": ["where", "sqlplus"], - }, - "sqlserver": { - "macos": ["which", "sqlcmd"], - "windows": ["where", "sqlcmd"], - }, -} - - -def is_python_environment() -> bool: - """현재 환경이 Python 실행 환경인지 확인""" - if sys.executable: - return True - if shutil.which("python3") or shutil.which("python"): - return True - return False - - -def check_os_driver_installed(driver_id: str) -> bool: - """Python 환경이 아닐 때 OS 레벨에서 DB 클라이언트 도구 설치 여부 확인""" - system = platform.system().lower() - driver_key = driver_id.lower() - - if driver_key not in OS_DRIVER_CHECK_COMMANDS: - return False # 지원하지 않는 드라이버 - - if system == "darwin": - cmd = OS_DRIVER_CHECK_COMMANDS[driver_key]["macos"] - elif system == "windows": - cmd = OS_DRIVER_CHECK_COMMANDS[driver_key]["windows"] - else: - return False # Linux 등은 필요 시 추가 - - try: - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - return bool(result.stdout.strip()) - except Exception: - return False - - -def check_driver(driver_id: str) -> DBDriverInfo: - driver_key = driver_id.lower() - module_names = DRIVER_MAP.get(driver_key) - - if not module_names: - return DBDriverInfo( - db_type=driver_id, - is_installed=False, - message="지원되지 않는 DB 타입입니다.", - driver_path=None, - driver_name=None, - driver_size_bytes=None, - driver_version=None, - os_name=os_simple_name, - os_full_name=os_full_name, - ) - - logger.info(f"[check_driver] 요청된 드라이버: {driver_id} → 모듈명: {module_names}") - logger.info(f"[check_driver] Python 환경 여부: {is_python_environment()}") - - if not is_python_environment(): - os_installed = check_os_driver_installed(driver_key) - return DBDriverInfo( - db_type=driver_id, - is_installed=os_installed, - message=( - "Python 환경이 아니어서 OS 레벨로 설치 여부를 확인했습니다." - if os_installed - else "Python 환경이 아니며 OS 레벨에서도 드라이버를 찾을 수 없습니다." - ), - driver_path=None, - driver_name=None, - driver_size_bytes=None, - driver_version=None, - os_name=os_simple_name, - os_full_name=os_full_name, - ) - - # module_names가 리스트일 때 첫 발견된 드라이버를 찾음 - if isinstance(module_names, str): - module_names = [module_names] - - installed_module = None - - spec = None - - for mod_name in module_names: - try: - mod = importlib.import_module(mod_name) - installed_module = mod_name - spec = getattr(mod, "__spec__", None) - break - except ModuleNotFoundError: - continue - - is_installed = installed_module is not None - logger.info(f"[check_driver] 발견된 모듈명: {installed_module}, spec: {spec}") - - driver_path = None - driver_name = None - driver_size_bytes = None - driver_version = "N/A" - - # print("DEBUG: spec=", spec) - # print("DEBUG: is_installed=", is_installed) - - if is_installed and spec and spec.origin: - driver_path = spec.origin - driver_name = os.path.basename(driver_path) - try: - driver_size_bytes = os.path.getsize(driver_path) - except Exception: - driver_size_bytes = None - - try: - mod = importlib.import_module(installed_module) - driver_version = getattr(mod, "__version__", "Unknown") - except Exception: - driver_version = "Unknown" - - return DBDriverInfo( - db_type=driver_id, - is_installed=is_installed, - message="드라이버가 설치되어 있습니다." if is_installed else "드라이버가 설치되어 있지 않습니다.", - driver_path=driver_path, - driver_name=driver_name, - driver_size_bytes=driver_size_bytes, - driver_version=driver_version, - os_name=os_simple_name, - os_full_name=os_full_name, - ) diff --git a/app/services/driver_info_provider.py b/app/services/driver_info_provider.py new file mode 100644 index 0000000..5fd0716 --- /dev/null +++ b/app/services/driver_info_provider.py @@ -0,0 +1,45 @@ +import importlib +import logging +import os + +DRIVER_MAP = { + "postgresql": ["psycopg2", "pg8000"], + "mysql": ["pymysql", "mysql.connector"], + "sqlite": ["sqlite3"], + "oracle": ["cx_Oracle"], + "sqlserver": ["pyodbc"], + "mariadb": ["pymysql", "mysql.connector"], +} + + +def db_driver_info(driver_id: str) -> dict: + driver_key = driver_id.lower() + module_names = DRIVER_MAP.get(driver_key) + + if not module_names: + # 지원되지 않는 DB 타입 + return {"message": "지원되지 않는 DB입니다.", "data": None} + + for mod_name in module_names: + try: + mod = importlib.import_module(mod_name) + version = getattr(mod, "__version__", None) + path = getattr(mod.__spec__, "origin", None) + size = os.path.getsize(path) if path else None + + return { + "message": "드라이버 정보를 성공적으로 불러왔습니다.", + "data": { + "db_type": driver_id, + "is_installed": True, + "driver_name": mod_name, + "driver_version": version, + "driver_size_bytes": size, + }, + } + except (ModuleNotFoundError, AttributeError, OSError) as e: + logging.warning(f"드라이버 '{mod_name}' import 실패: {e}") + continue + + # import 실패한 경우 + return {"message": "드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요.", "data": None} diff --git a/app/tests/api/test_driver_check.py b/app/tests/api/test_driver_check.py deleted file mode 100644 index 1620ed6..0000000 --- a/app/tests/api/test_driver_check.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -import subprocess -import sys -from unittest.mock import MagicMock, patch - -from fastapi.testclient import TestClient - -from app.main import app -from app.services.driver_checker import check_driver - -print(f"Python 실행 경로: {sys.executable}") -client = TestClient(app) - - -@patch("app.services.driver_checker.importlib.import_module") -@patch("os.path.getsize", return_value=123456) -@patch("app.services.driver_checker.is_python_environment", return_value=True) -def test_check_mysql_driver_installed(mock_env, mock_getsize, mock_import_module): - # ✅ 모듈과 spec 모킹 - spec_mock = MagicMock() - spec_mock.origin = "/usr/lib/python3.11/site-packages/mysql_driver.so" - - mock_module = MagicMock() - mock_module.__spec__ = spec_mock - mock_module.__version__ = "8.0.0" - mock_import_module.return_value = mock_module - - from app.main import app - - client = TestClient(app) - response = client.get("/connections/drivers/mysql") - - data = response.json() - print(json.dumps(data, indent=4, ensure_ascii=False)) - - assert data["is_installed"] is True - assert data["driver_path"] == spec_mock.origin - assert data["driver_size_bytes"] == 123456 - assert data["driver_version"] == "8.0.0" - assert data["driver_name"] == "mysql_driver.so" - - -def test_check_mysql_driver_not_installed_python_env(): - mock_spec = None # 드라이버 미설치 상황에서는 None - - with patch("app.services.driver_checker.is_python_environment", return_value=True): - with patch("app.services.driver_checker.importlib.util.find_spec", return_value=mock_spec): - response = client.get("/connections/drivers/mysql") - assert response.status_code == 200 - data = response.json() - print(json.dumps(data, indent=4, ensure_ascii=False)) - assert data["db_type"] == "mysql" - assert data["is_installed"] is False - assert data["driver_path"] is None - assert data["driver_name"] is None - assert data["driver_size_bytes"] is None - assert "설치되어 있지 않습니다" in data["message"] - - -def test_check_driver_not_python_environment_driver_installed(): - with patch("app.services.driver_checker.is_python_environment", return_value=False): - mock_subproc_result = subprocess.CompletedProcess( - args=["which", "mysql"], returncode=0, stdout=b"/usr/bin/mysql\n" - ) - with patch("subprocess.run", return_value=mock_subproc_result): - response = client.get("/connections/drivers/mysql") - assert response.status_code == 200 - data = response.json() - print(json.dumps(data, indent=4, ensure_ascii=False)) - assert data["db_type"] == "mysql" - assert data["is_installed"] is True - assert "Python 환경이 아니어서 OS 레벨로 설치 여부를 확인했습니다." in data["message"] - - -def test_check_driver_not_python_environment_driver_not_installed(): - with patch("app.services.driver_checker.is_python_environment", return_value=False): - mock_subproc_result = subprocess.CompletedProcess(args=["which", "mysql"], returncode=1, stdout=b"") - with patch("subprocess.run", return_value=mock_subproc_result): - response = client.get("/connections/drivers/mysql") - assert response.status_code == 200 - data = response.json() - print(json.dumps(data, indent=4, ensure_ascii=False)) - assert data["db_type"] == "mysql" - assert data["is_installed"] is False - assert "Python 환경이 아니며 OS 레벨에서도 드라이버를 찾을 수 없습니다." in data["message"] - - -def test_check_driver_unsupported_db_direct(): - unsupported_db = "unknown_db" - response = check_driver(unsupported_db) - - print(json.dumps(response.model_dump(), indent=4, ensure_ascii=False)) - - assert response.db_type == unsupported_db - assert response.is_installed is False - assert response.message == "지원되지 않는 DB 타입입니다." - assert response.driver_path is None - assert response.driver_name is None - assert response.driver_size_bytes is None - assert response.driver_version is None diff --git a/app/tests/api/test_environment.py b/app/tests/api/test_environment.py deleted file mode 100644 index da9a454..0000000 --- a/app/tests/api/test_environment.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi.testclient import TestClient - -from app.main import app - -client = TestClient(app) - - -def test_check_python_environment_api(): - response = client.get("/environment/python") - assert response.status_code == 200 - data = response.json() - print("[python env result]", data) - - assert "is_python_environment" in data - assert isinstance(data["is_python_environment"], bool) From 5dd1832e272bc36005e374a1db4170720000257b Mon Sep 17 00:00:00 2001 From: mini Date: Fri, 25 Jul 2025 02:02:35 +0900 Subject: [PATCH 11/34] =?UTF-8?q?test:=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=20=EC=A0=95=EB=B3=B4=20=ED=99=95=EC=9D=B8=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=EB=8B=A8=EC=9C=84=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8,=20api=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A7=84?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tests/api/test_driver_info_provider.py | 27 ++++++++++++ app/tests/unit/test_driver_info_provider.py | 47 +++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 app/tests/api/test_driver_info_provider.py create mode 100644 app/tests/unit/test_driver_info_provider.py diff --git a/app/tests/api/test_driver_info_provider.py b/app/tests/api/test_driver_info_provider.py new file mode 100644 index 0000000..ece56f3 --- /dev/null +++ b/app/tests/api/test_driver_info_provider.py @@ -0,0 +1,27 @@ +from fastapi.testclient import TestClient + +from app.main import app # FastAPI 앱 객체 + +client = TestClient(app) + + +def test_api_supported_driver(): + response = client.get("/connections/drivers/mysql") + assert response.status_code == 200 + body = response.json() + assert body["message"] == "드라이버 정보를 성공적으로 불러왔습니다." + assert body["data"]["db_type"] == "mysql" + assert body["data"]["is_installed"] is True + + +def test_api_unsupported_driver(): + response = client.get("/connections/drivers/unknown-db") + assert response.status_code == 200 + body = response.json() + assert body["message"] == "지원되지 않는 DB입니다." + assert body["data"] is None + + +def test_api_empty_driver(): + response = client.get("/connections/drivers/") + assert response.status_code in [404, 422] # 경로 누락이므로 상태코드로 판단 diff --git a/app/tests/unit/test_driver_info_provider.py b/app/tests/unit/test_driver_info_provider.py new file mode 100644 index 0000000..d85e044 --- /dev/null +++ b/app/tests/unit/test_driver_info_provider.py @@ -0,0 +1,47 @@ +import pytest + +from app.services.driver_info_provider import db_driver_info + + +# 설치된 DB 드라이버 중 하나를 테스트 (환경에 따라 달라질 수 있음) +def test_supported_driver_installed(): + result = db_driver_info("mysql") + assert result["message"] == "드라이버 정보를 성공적으로 불러왔습니다." + assert result["data"] is not None + assert result["data"]["db_type"] == "mysql" + assert result["data"]["is_installed"] is True + assert result["data"]["driver_name"] in ["pymysql", "mysql.connector"] + + +# 존재하지 않는 DB 타입을 넘겼을 때 +def test_unsupported_driver(): + result = db_driver_info("unknown-db") + assert result["message"] == "지원되지 않는 DB입니다." + assert result["data"] is None + + +# 빈 값 넘겼을 때 +def test_empty_input(): + result = db_driver_info("") + assert result["message"] == "지원되지 않는 DB입니다." + assert result["data"] is None + + +# 지원은 하지만 환경에 설치되지 않은 드라이버를 일부러 테스트 +@pytest.mark.skip(reason="환경에 따라 설치 여부가 다르므로 건너뜀") +def test_supported_but_not_installed(): + result = db_driver_info("oracle") + assert result["message"] in [ + "드라이버 정보를 성공적으로 불러왔습니다.", + "드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요.", + ] + + +# 함수 강제 실패 테스트 +def test_import_fails_and_fallback_message(): + from unittest import mock + + with mock.patch("importlib.import_module", side_effect=ModuleNotFoundError("모듈 없음")): + result = db_driver_info("mysql") + assert result["message"] == "드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요." + assert result["data"] is None From 26b9c64b0eec324b3061eec2d66dda23f2913eba Mon Sep 17 00:00:00 2001 From: mini Date: Fri, 25 Jul 2025 02:16:10 +0900 Subject: [PATCH 12/34] =?UTF-8?q?test:=20=EC=BD=94=EB=93=9C=20=EC=BB=A4?= =?UTF-8?q?=EB=B2=84=EB=A6=AC=EC=A7=80=20=EC=B8=A1=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...fo_provider.py => test_driver_info_api.py} | 0 ...o_provider.py => test_driver_info_unit.py} | 0 poetry.lock | 123 +++++++++++++++++- pyproject.toml | 1 + 4 files changed, 123 insertions(+), 1 deletion(-) rename app/tests/api/{test_driver_info_provider.py => test_driver_info_api.py} (100%) rename app/tests/unit/{test_driver_info_provider.py => test_driver_info_unit.py} (100%) diff --git a/app/tests/api/test_driver_info_provider.py b/app/tests/api/test_driver_info_api.py similarity index 100% rename from app/tests/api/test_driver_info_provider.py rename to app/tests/api/test_driver_info_api.py diff --git a/app/tests/unit/test_driver_info_provider.py b/app/tests/unit/test_driver_info_unit.py similarity index 100% rename from app/tests/unit/test_driver_info_provider.py rename to app/tests/unit/test_driver_info_unit.py diff --git a/poetry.lock b/poetry.lock index 07b59ee..1ad3eb7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,6 +144,107 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +[[package]] +name = "coverage" +version = "7.10.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbd823f7ea5286c26406ad9e54268544d82f3d1cadb6d4f3b85e9877f0cab1ef"}, + {file = "coverage-7.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab3f7a5dbaab937df0b9e9e8ec6eab235ba9a6f29d71fd3b24335affaed886cc"}, + {file = "coverage-7.10.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8c63aaf850523d8cbe3f5f1a5c78f689b223797bef902635f2493ab43498f36c"}, + {file = "coverage-7.10.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c3133ce3fa84023f7c6921c4dca711be0b658784c5a51a797168229eae26172"}, + {file = "coverage-7.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3747d1d0af85b17d3a156cd30e4bbacf893815e846dc6c07050e9769da2b138e"}, + {file = "coverage-7.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:241923b350437f6a7cb343d9df72998305ef940c3c40009f06e05029a047677c"}, + {file = "coverage-7.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13e82e499309307104d58ac66f9eed237f7aaceab4325416645be34064d9a2be"}, + {file = "coverage-7.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bf73cdde4f6c9cd4457b00bf1696236796ac3a241f859a55e0f84a4c58326a7f"}, + {file = "coverage-7.10.0-cp310-cp310-win32.whl", hash = "sha256:2396e13275b37870a3345f58bce8b15a7e0a985771d13a4b16ce9129954e07d6"}, + {file = "coverage-7.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d45c7c71fb3d2da92ab893602e3f28f2d1560cec765a27e1824a6e0f7e92cfd"}, + {file = "coverage-7.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4abc01843581a6f9dd72d4d15761861190973a2305416639435ef509288f7a04"}, + {file = "coverage-7.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2093297773111d7d748fe4a99b68747e57994531fb5c57bbe439af17c11c169"}, + {file = "coverage-7.10.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58240e27815bf105bd975c2fd42e700839f93d5aad034ef976411193ca32dbfd"}, + {file = "coverage-7.10.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d019eac999b40ad48521ea057958b07a9f549c0c6d257a20e5c7c4ba91af8d1c"}, + {file = "coverage-7.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0a1f5454bc80faf4ceab10d1d48f025f92046c9c0f3bec2e1a9dda55137f8"}, + {file = "coverage-7.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a93dd7759c416dd1cc754123b926d065055cb9a33b6699e64a1e5bdfae1ff459"}, + {file = "coverage-7.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7b3d737266048368a6ffd68f1ecd662c54de56535c82eb8f98a55ac216a72cbd"}, + {file = "coverage-7.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93227c2707cb0effd9163cd0d8f0d9ab628982f7a3e915d6d64c7107867b9a07"}, + {file = "coverage-7.10.0-cp311-cp311-win32.whl", hash = "sha256:69270af3014ab3058ad6108c6d0e218166f568b5a7a070dc3d62c0a63aca1c4d"}, + {file = "coverage-7.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c16bbb661a7b4dafac0ab69e44d6dbcc6a64c4d93aefd89edc6f8911b6ab4a"}, + {file = "coverage-7.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:14e7c23fcb74ed808efb4eb48fcd25a759f0e20f685f83266d1df174860e4733"}, + {file = "coverage-7.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2adcfdaf3b4d69b0c64ad024fe9dd6996782b52790fb6033d90f36f39e287df"}, + {file = "coverage-7.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d7b27c2c0840e8eeff3f1963782bd9d3bc767488d2e67a31de18d724327f9f6"}, + {file = "coverage-7.10.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0ed50429786e935517570b08576a661fd79032e6060985ab492b9d39ba8e66ee"}, + {file = "coverage-7.10.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7171c139ab6571d70460ecf788b1dcaf376bfc75a42e1946b8c031d062bbbad4"}, + {file = "coverage-7.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a726aac7e6e406e403cdee4c443a13aed3ea3d67d856414c5beacac2e70c04e"}, + {file = "coverage-7.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2886257481a14e953e96861a00c0fe7151117a523f0470a51e392f00640bba03"}, + {file = "coverage-7.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:536578b79521e59c385a2e0a14a5dc2a8edd58761a966d79368413e339fc9535"}, + {file = "coverage-7.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77fae95558f7804a9ceefabf3c38ad41af1da92b39781b87197c6440dcaaa967"}, + {file = "coverage-7.10.0-cp312-cp312-win32.whl", hash = "sha256:97803e14736493eb029558e1502fe507bd6a08af277a5c8eeccf05c3e970cb84"}, + {file = "coverage-7.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c73ab554e54ffd38d114d6bc4a7115fb0c840cf6d8622211bee3da26e4bd25d"}, + {file = "coverage-7.10.0-cp312-cp312-win_arm64.whl", hash = "sha256:3ae95d5a9aedab853641026b71b2ddd01983a0a7e9bf870a20ef3c8f5d904699"}, + {file = "coverage-7.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d883fee92b9245c0120fa25b5d36de71ccd4cfc29735906a448271e935d8d86d"}, + {file = "coverage-7.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c87e59e88268d30e33d3665ede4fbb77b513981a2df0059e7c106ca3de537586"}, + {file = "coverage-7.10.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f669d969f669a11d6ceee0b733e491d9a50573eb92a71ffab13b15f3aa2665d4"}, + {file = "coverage-7.10.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9582bd6c6771300a847d328c1c4204e751dbc339a9e249eecdc48cada41f72e6"}, + {file = "coverage-7.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91f97e9637dc7977842776fdb7ad142075d6fa40bc1b91cb73685265e0d31d32"}, + {file = "coverage-7.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae4fa92b6601a62367c6c9967ad32ad4e28a89af54b6bb37d740946b0e0534dd"}, + {file = "coverage-7.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3a5cc8b97473e7b3623dd17a42d2194a2b49de8afecf8d7d03c8987237a9552c"}, + {file = "coverage-7.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc1cbb7f623250e047c32bd7aa1bb62ebc62608d5004d74df095e1059141ac88"}, + {file = "coverage-7.10.0-cp313-cp313-win32.whl", hash = "sha256:1380cc5666d778e77f1587cd88cc317158111f44d54c0dd3975f0936993284e0"}, + {file = "coverage-7.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:bf03cf176af098ee578b754a03add4690b82bdfe070adfb5d192d0b1cd15cf82"}, + {file = "coverage-7.10.0-cp313-cp313-win_arm64.whl", hash = "sha256:8041c78cd145088116db2329b2fb6e89dc338116c962fbe654b7e9f5d72ab957"}, + {file = "coverage-7.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37cc2c06052771f48651160c080a86431884db9cd62ba622cab71049b90a95b3"}, + {file = "coverage-7.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:91f37270b16178b05fa107d85713d29bf21606e37b652d38646eef5f2dfbd458"}, + {file = "coverage-7.10.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f9b0b0168864d09bcb9a3837548f75121645c4cfd0efce0eb994c221955c5b10"}, + {file = "coverage-7.10.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0be435d3b616e7d3ee3f9ebbc0d784a213986fe5dff9c6f1042ee7cfd30157"}, + {file = "coverage-7.10.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e9aba1c4434b837b1d567a533feba5ce205e8e91179c97974b28a14c23d3a0"}, + {file = "coverage-7.10.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a0b0c481e74dfad631bdc2c883e57d8b058e5c90ba8ef087600995daf7bbec18"}, + {file = "coverage-7.10.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8aec1b7c8922808a433c13cd44ace6fceac0609f4587773f6c8217a06102674b"}, + {file = "coverage-7.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:04ec59ceb3a594af0927f2e0d810e1221212abd9a2e6b5b917769ff48760b460"}, + {file = "coverage-7.10.0-cp313-cp313t-win32.whl", hash = "sha256:b6871e62d29646eb9b3f5f92def59e7575daea1587db21f99e2b19561187abda"}, + {file = "coverage-7.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff99cff2be44f78920b76803f782e91ffb46ccc7fa89eccccc0da3ca94285b64"}, + {file = "coverage-7.10.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3246b63501348fe47299d12c47a27cfc221cfbffa1c2d857bcc8151323a4ae4f"}, + {file = "coverage-7.10.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:1f628d91f941a375b4503cb486148dbeeffb48e17bc080e0f0adfee729361574"}, + {file = "coverage-7.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a0e101d5af952d233557e445f42ebace20b06b4ceb615581595ced5386caa78"}, + {file = "coverage-7.10.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ec4c1abbcc53f9f650acb14ea71725d88246a9e14ed42f8dd1b4e1b694e9d842"}, + {file = "coverage-7.10.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9c95f3a7f041b4cc68a8e3fecfa6366170c13ac773841049f1cd19c8650094e0"}, + {file = "coverage-7.10.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a2cd597b69c16d24e310611f2ed6fcfb8f09429316038c03a57e7b4f5345244"}, + {file = "coverage-7.10.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5e18591906a40c2b3609196c9879136aa4a47c5405052ca6b065ab10cb0b71d0"}, + {file = "coverage-7.10.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:485c55744252ed3f300cc1a0f5f365e684a0f2651a7aed301f7a67125906b80e"}, + {file = "coverage-7.10.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4dabea1516e5b0e9577282b149c8015e4dceeb606da66fb8d9d75932d5799bf5"}, + {file = "coverage-7.10.0-cp314-cp314-win32.whl", hash = "sha256:ac455f0537af22333fdc23b824cff81110dff2d47300bb2490f947b7c9a16017"}, + {file = "coverage-7.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:b3c94b532f52f95f36fbfde3e178510a4d04eea640b484b2fe8f1491338dc653"}, + {file = "coverage-7.10.0-cp314-cp314-win_arm64.whl", hash = "sha256:2f807f2c3a9da99c80dfa73f09ef5fc3bd21e70c73ba1c538f23396a3a772252"}, + {file = "coverage-7.10.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0a889ef25215990f65073c32cadf37483363a6a22914186dedc15a6b1a597d50"}, + {file = "coverage-7.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39c638ecf3123805bacbf71aff8091e93af490c676fca10ab4e442375076e483"}, + {file = "coverage-7.10.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2f2c0df0cbcf7dffa14f88a99c530cdef3f4fcfe935fa4f95d28be2e7ebc570"}, + {file = "coverage-7.10.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:048d19a5d641a2296745ab59f34a27b89a08c48d6d432685f22aac0ec1ea447f"}, + {file = "coverage-7.10.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1209b65d302d7a762004be37ab9396cbd8c99525ed572bdf455477e3a9449e06"}, + {file = "coverage-7.10.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e44aa79a36a7a0aec6ea109905a4a7c28552d90f34e5941b36217ae9556657d5"}, + {file = "coverage-7.10.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96124be864b89395770c9a14652afcddbcdafb99466f53a9281c51d1466fb741"}, + {file = "coverage-7.10.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aad222e841f94b42bd1d6be71737fade66943853f0807cf87887c88f70883a2a"}, + {file = "coverage-7.10.0-cp314-cp314t-win32.whl", hash = "sha256:0eed5354d28caa5c8ad60e07e938f253e4b2810ea7dd56784339b6ce98b6f104"}, + {file = "coverage-7.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3da35f9980058acb960b2644527cc3911f1e00f94d309d704b309fa984029109"}, + {file = "coverage-7.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cb9e138dfa8a4b5c52c92a537651e2ca4f2ca48d8cb1bc01a2cbe7a5773c2426"}, + {file = "coverage-7.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cf283ec9c6878826291b17442eb5c32d3d252dc77d25e082b460b2d2ea67ba3c"}, + {file = "coverage-7.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a83488c9fc6fff487f2ab551f9b64c70672357b8949f0951b0cd778b3ed8165"}, + {file = "coverage-7.10.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b86df3a7494d12338c11e59f210a0498d6109bbc3a4037f44de517ebb30a9c6b"}, + {file = "coverage-7.10.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6de9b460809e5e4787b742e786a36ae2346a53982e2be317cdcb7a33c56412fb"}, + {file = "coverage-7.10.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de5ef8a5954d63fa26a6aaa4600e48f885ce70fe495e8fce2c43aa9241fc9434"}, + {file = "coverage-7.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f178fe5e96f1e057527d5d0b20ab76b8616e0410169c33716cc226118eaf2c4f"}, + {file = "coverage-7.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a38c42f0182a012fa9ec25bc6057e51114c1ba125be304f3f776d6d283cb303"}, + {file = "coverage-7.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bf09beb5c1785cb36aad042455c0afab561399b74bb8cdaf6e82b7d77322df99"}, + {file = "coverage-7.10.0-cp39-cp39-win32.whl", hash = "sha256:cb8dfbb5d3016cb8d1940444c0c69b40cdc6c8bde724b07716ee5ea47b5273c6"}, + {file = "coverage-7.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:58ff22653cd93d563110d1ff2aef958f5f21be9e917762f8124d0e36f80f172a"}, + {file = "coverage-7.10.0-py3-none-any.whl", hash = "sha256:310a786330bb0463775c21d68e26e79973839b66d29e065c5787122b8dd4489f"}, + {file = "coverage-7.10.0.tar.gz", hash = "sha256:2768885aef484b5dcde56262cbdfba559b770bfc46994fe9485dc3614c7a5867"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "cx-oracle" version = "8.3.0" @@ -946,6 +1047,26 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-cov" +version = "6.2.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=6.2.5" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pywin32-ctypes" version = "0.2.3" @@ -1269,4 +1390,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "b74b3112c796bc9e08554482dd7dc61da090b1788706803ec8c2b9d7668fd563" +content-hash = "b54ff38df6da37302c0517493eeedc366a91ccbb2f4ef7cec1185e0fd83b6e3e" diff --git a/pyproject.toml b/pyproject.toml index 0f8cca5..fbc9141 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ pytest-asyncio = "^1.1.0" # ---------------------------- # Ruff 설정 # ---------------------------- +pytest-cov = "^6.2.1" [tool.ruff] line-length = 120 exclude = [ From a589add570970cae2f65ed897aea43e49a62614b Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:17:53 +0900 Subject: [PATCH 13/34] =?UTF-8?q?feat:=20=EB=8F=99=EC=A0=81=20=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=ED=95=A0=EB=8B=B9=20->=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/port.py | 23 ----------------------- app/main.py | 16 +++------------- 2 files changed, 3 insertions(+), 36 deletions(-) delete mode 100644 app/core/port.py diff --git a/app/core/port.py b/app/core/port.py deleted file mode 100644 index 8305493..0000000 --- a/app/core/port.py +++ /dev/null @@ -1,23 +0,0 @@ -# app/core/port.py - -import os -import socket - - -def get_available_port(default: int = 8000) -> int: - """ - 환경변수 'PORT'가 존재하면 해당 포트를 사용하고, - 없다면 시스템이 할당한 사용 가능한 포트를 반환합니다. - """ - port_from_env = os.getenv("PORT") - - if port_from_env: - print(f"Using port from environment variable: {port_from_env}") - return int(port_from_env) - - # 포트 0 바인딩 → 시스템이 사용 가능한 포트 할당 - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("0.0.0.0", 0)) - assigned_port = s.getsockname()[1] - print(f"Dynamically assigned port: {assigned_port}") - return assigned_port diff --git a/app/main.py b/app/main.py index c20f561..831928a 100644 --- a/app/main.py +++ b/app/main.py @@ -4,12 +4,11 @@ from fastapi import FastAPI from app.api import ( - connections, + connect_driver, # 드라이버 확인 health, # 헬스 체크 ) from app.api.api_router import api_router from app.core.exceptions import APIException, api_exception_handler, generic_exception_handler -from app.core.port import get_available_port # 동적 포트 할당 app = FastAPI() @@ -17,21 +16,12 @@ app.add_exception_handler(APIException, api_exception_handler) app.add_exception_handler(Exception, generic_exception_handler) -# 드라이버 확인 라우터 -app.include_router(connections.router) - # 라우터 app.include_router(health.router) app.include_router(api_router, prefix="/api") - - -@app.get("/") -async def read_root(): - return {"message": "Hello, FastAPI Backend!"} +app.include_router(connect_driver.router, prefix="/api") if __name__ == "__main__": - # 동적 할당 로직 - port = get_available_port() # Uvicorn 서버를 시작합니다. - uvicorn.run(app, host="0.0.0.0", port=port) + uvicorn.run(app, host="0.0.0.0", port=39722) From 8283e13faebd0899a0389c12ce0b4c0e2c993555 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:22:02 +0900 Subject: [PATCH 14/34] =?UTF-8?q?feat:=20API=EC=97=90=EC=84=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EB=93=9C=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80=20(DriverEn?= =?UTF-8?q?um)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connect_driver.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/api/connect_driver.py diff --git a/app/api/connect_driver.py b/app/api/connect_driver.py new file mode 100644 index 0000000..d2e1ff8 --- /dev/null +++ b/app/api/connect_driver.py @@ -0,0 +1,19 @@ +# app/api/connect_driver.py + +from fastapi import APIRouter + +from app.schemas.driver_info import DriverEnum, DriverInfoResponse +from app.services.driver_info_provider import db_driver_info + +router = APIRouter(tags=["Driver"]) + + +@router.get( + "/connections/drivers/{driver_id}", + summary="DB 드라이버 정보 조회 API", + response_model=DriverInfoResponse, +) +def read_driver_info(driver_id: DriverEnum): + module = driver_id.driver_module + db_type = driver_id.value + return db_driver_info(db_type, module) From 65b9a5230cc99b861f6bf41d8298380df83e79d9 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:25:02 +0900 Subject: [PATCH 15/34] =?UTF-8?q?feat:=20DriverEnum=20=EB=B0=8F=20DriverIn?= =?UTF-8?q?fo=20=EC=9D=91=EB=8B=B5=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/driver_info.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/schemas/driver_info.py diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py new file mode 100644 index 0000000..5478ba4 --- /dev/null +++ b/app/schemas/driver_info.py @@ -0,0 +1,42 @@ +# app/schemas/driver_schemas.py +from enum import Enum + +from pydantic import BaseModel + + +class DriverEnum(str, Enum): + postgresql = ("postgresql", "psycopg2") + mysql = ("mysql", "mysql.connector") + sqlite = ("sqlite", "sqlite3") + oracle = ("oracle", "cx_Oracle") + sqlserver = ("sqlserver", "pyodbc") + mariadb = ("mariadb", "pymysql") + + def __new__(cls, db_type, module): + obj = str.__new__(cls, db_type) + obj._value_ = db_type # 초기 유효성 검사 값 + obj.driver_module = module # db_type에 맞는 드라이버 + return obj + + +class DriverInfo(BaseModel): + db_type: str + is_installed: bool + driver_name: str | None + driver_version: str | None + driver_size_bytes: int | None + + +class DriverInfoResponse(BaseModel): + message: str + data: DriverInfo | None = None + + @classmethod + def success(cls, value: DriverInfo): + return cls(message="드라이버 정보를 성공적으로 불러왔습니다.", data=value) + + @classmethod + def error(cls, e: Exception | None = None): + if e: + print(f"[ERROR] {type(e).__name__}: {e}") # 로그 + return cls(message="드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요.", data=None) From 36fb86b6eb19d3d04a9267e3453c7fdb743140c6 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:27:22 +0900 Subject: [PATCH 16/34] =?UTF-8?q?refactor:=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=20=EC=A0=95=EB=B3=B4=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?dict=20=E2=86=92=20=EB=AA=A8=EB=8D=B8=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/driver_info_provider.py | 57 ++++++++++------------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/app/services/driver_info_provider.py b/app/services/driver_info_provider.py index 5fd0716..f702735 100644 --- a/app/services/driver_info_provider.py +++ b/app/services/driver_info_provider.py @@ -1,45 +1,26 @@ +# app/service/driver_info_provider.py + import importlib -import logging import os -DRIVER_MAP = { - "postgresql": ["psycopg2", "pg8000"], - "mysql": ["pymysql", "mysql.connector"], - "sqlite": ["sqlite3"], - "oracle": ["cx_Oracle"], - "sqlserver": ["pyodbc"], - "mariadb": ["pymysql", "mysql.connector"], -} - - -def db_driver_info(driver_id: str) -> dict: - driver_key = driver_id.lower() - module_names = DRIVER_MAP.get(driver_key) +from app.schemas.driver_info import DriverInfo, DriverInfoResponse - if not module_names: - # 지원되지 않는 DB 타입 - return {"message": "지원되지 않는 DB입니다.", "data": None} - for mod_name in module_names: - try: - mod = importlib.import_module(mod_name) - version = getattr(mod, "__version__", None) - path = getattr(mod.__spec__, "origin", None) - size = os.path.getsize(path) if path else None +def db_driver_info(db_type: str, module_name: str) -> DriverInfoResponse: + try: + mod = importlib.import_module(module_name) + version = getattr(mod, "__version__", None) + path = getattr(mod.__spec__, "origin", None) + size = os.path.getsize(path) if path else None - return { - "message": "드라이버 정보를 성공적으로 불러왔습니다.", - "data": { - "db_type": driver_id, - "is_installed": True, - "driver_name": mod_name, - "driver_version": version, - "driver_size_bytes": size, - }, - } - except (ModuleNotFoundError, AttributeError, OSError) as e: - logging.warning(f"드라이버 '{mod_name}' import 실패: {e}") - continue + info = DriverInfo( + db_type=db_type, + is_installed=True, + driver_name=module_name, + driver_version=version, + driver_size_bytes=size, + ) + return DriverInfoResponse.success(info) - # import 실패한 경우 - return {"message": "드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요.", "data": None} + except (ModuleNotFoundError, AttributeError, OSError) as e: + return DriverInfoResponse.error(e) From 0d1a86f79495fa0cc4919cb812d17f770470b6fe Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:28:33 +0900 Subject: [PATCH 17/34] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD(connectioins.py=20->=20connect=5Fdriver.p?= =?UTF-8?q?y)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connections.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 app/api/connections.py diff --git a/app/api/connections.py b/app/api/connections.py deleted file mode 100644 index ce99b9b..0000000 --- a/app/api/connections.py +++ /dev/null @@ -1,10 +0,0 @@ -from fastapi import APIRouter - -from app.services.driver_info_provider import db_driver_info - -router = APIRouter() - - -@router.get("/connections/drivers/{driverId}") -def read_driver_info(driverId: str): - return db_driver_info(driverId) From d4475db37370f2cf116a863790f4d5714741cbb3 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:29:24 +0900 Subject: [PATCH 18/34] =?UTF-8?q?refactor:=20=EB=B3=80=EA=B2=BD=EB=90=9C?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tests/api/test_driver_info_api.py | 18 ++++---- app/tests/unit/test_driver_info_unit.py | 55 +++++++++---------------- 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/app/tests/api/test_driver_info_api.py b/app/tests/api/test_driver_info_api.py index ece56f3..f64083a 100644 --- a/app/tests/api/test_driver_info_api.py +++ b/app/tests/api/test_driver_info_api.py @@ -6,22 +6,26 @@ def test_api_supported_driver(): - response = client.get("/connections/drivers/mysql") + response = client.get("api/connections/drivers/mysql") assert response.status_code == 200 + body = response.json() assert body["message"] == "드라이버 정보를 성공적으로 불러왔습니다." assert body["data"]["db_type"] == "mysql" assert body["data"]["is_installed"] is True + assert body["data"]["driver_name"] == "mysql.connector" + assert isinstance(body["data"]["driver_size_bytes"], int) def test_api_unsupported_driver(): - response = client.get("/connections/drivers/unknown-db") - assert response.status_code == 200 + response = client.get("api/connections/drivers/unknown-db") + assert response.status_code == 422 # ❗ Enum validation이 터짐 + body = response.json() - assert body["message"] == "지원되지 않는 DB입니다." - assert body["data"] is None + assert body["detail"][0]["msg"].startswith("Input should be") + assert body["detail"][0]["loc"] == ["path", "driver_id"] def test_api_empty_driver(): - response = client.get("/connections/drivers/") - assert response.status_code in [404, 422] # 경로 누락이므로 상태코드로 판단 + response = client.get("api/connections/drivers/") # 누락된 경로 + assert response.status_code in [404, 422] diff --git a/app/tests/unit/test_driver_info_unit.py b/app/tests/unit/test_driver_info_unit.py index d85e044..93b53bd 100644 --- a/app/tests/unit/test_driver_info_unit.py +++ b/app/tests/unit/test_driver_info_unit.py @@ -1,47 +1,32 @@ -import pytest +from unittest import mock +from app.api.connect_driver import DriverEnum from app.services.driver_info_provider import db_driver_info -# 설치된 DB 드라이버 중 하나를 테스트 (환경에 따라 달라질 수 있음) +# 드라이버 설치 되었을 때 def test_supported_driver_installed(): - result = db_driver_info("mysql") - assert result["message"] == "드라이버 정보를 성공적으로 불러왔습니다." - assert result["data"] is not None - assert result["data"]["db_type"] == "mysql" - assert result["data"]["is_installed"] is True - assert result["data"]["driver_name"] in ["pymysql", "mysql.connector"] + driver = DriverEnum.mysql + result = db_driver_info(driver.value, driver.driver_module) + print(result) -# 존재하지 않는 DB 타입을 넘겼을 때 -def test_unsupported_driver(): - result = db_driver_info("unknown-db") - assert result["message"] == "지원되지 않는 DB입니다." - assert result["data"] is None +# 존재하지 않는 DB 타입을 넘겼을 때 (Enum 변환 실패) +def test_invalid_enum_value(): + try: + DriverEnum("nonexistent-driver") + assert False # 실패하지 않으면 오류! + except ValueError as e: + print(f"[Enum 변환 실패] {e}") # 콘솔에 예외 메시지 출력 + assert True -# 빈 값 넘겼을 때 -def test_empty_input(): - result = db_driver_info("") - assert result["message"] == "지원되지 않는 DB입니다." - assert result["data"] is None +# 빈 값 넘겼을 때 -> 제거: Enum 유효성 검사로 인해 필요성 없음 -# 지원은 하지만 환경에 설치되지 않은 드라이버를 일부러 테스트 -@pytest.mark.skip(reason="환경에 따라 설치 여부가 다르므로 건너뜀") -def test_supported_but_not_installed(): - result = db_driver_info("oracle") - assert result["message"] in [ - "드라이버 정보를 성공적으로 불러왔습니다.", - "드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요.", - ] - - -# 함수 강제 실패 테스트 -def test_import_fails_and_fallback_message(): - from unittest import mock - +# importlib 실패 상황 테스트 +def test_driver_import_failure_at_api(): with mock.patch("importlib.import_module", side_effect=ModuleNotFoundError("모듈 없음")): - result = db_driver_info("mysql") - assert result["message"] == "드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요." - assert result["data"] is None + driver = DriverEnum.mysql + result = db_driver_info(driver.value, driver.driver_module) + print(result) From 1450d6699853bd2ee77c41d042defe80d0952411 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:30:27 +0900 Subject: [PATCH 19/34] =?UTF-8?q?docs:=20Health=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=84=B0=EC=97=90=20Swagger=20=ED=83=9C=EA=B7=B8=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/health.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/health.py b/app/api/health.py index f88e530..2b20245 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -1,7 +1,7 @@ # app/api/health.py from fastapi import APIRouter -router = APIRouter() +router = APIRouter(tags=["Health"]) @router.get("/health") From 2170ad99d8f26e11035ed04d3c2a57d77a8a27a7 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 02:32:45 +0900 Subject: [PATCH 20/34] =?UTF-8?q?docs:=20README.md=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(=EB=8F=99=EC=A0=81=20=ED=8F=AC=ED=8A=B8=20=ED=95=A0=EB=8B=B9->?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=ED=8F=AC=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3c87aee..aa63744 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ 또는 Poetry Run을 사용하여 직접 실행할 수 있습니다. ```bash - poetry run uvicorn main:app --reload + uvicorn main:app --host 0.0.0.0 --port 39722 ``` ### **코드 컨벤션 (PEP 8, Ruff, Black)** @@ -141,9 +141,9 @@ 1. **브라우저 확인** - - 기본 루트 엔드포인트: - - 헬스 체크 엔드포인트: - - API 문서: + - 기본 루트 엔드포인트: + - 헬스 체크 엔드포인트: + - API 문서: 2. **CLI로 접속 확인하기** @@ -151,14 +151,13 @@ - 기본 루트 엔드포인트: ```bash - curl http://localhost:8000/ + curl http://localhost:39722/ ``` - 헬스 체크 엔드포인트: ```bash - curl http://localhost:8000/health + curl http://localhost:39722/health ``` - API 문서: ```bash - curl http://localhost:8000/openapi.json + curl http://localhost:39722/openapi.json ``` - From c0e76517a8246bc31003db64dba267f6cd833c86 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 27 Jul 2025 03:05:17 +0900 Subject: [PATCH 21/34] =?UTF-8?q?docs:=20README.md=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(=EB=8F=99=EC=A0=81=20=ED=8F=AC=ED=8A=B8=20=ED=95=A0=EB=8B=B9->?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=ED=8F=AC=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa63744..2295f16 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,13 @@ ```bash poetry shell - uvicorn app.main:app --reload + uvicorn app.main:app --host 0.0.0.0 --port 39722 --reload ``` 또는 Poetry Run을 사용하여 직접 실행할 수 있습니다. ```bash - uvicorn main:app --host 0.0.0.0 --port 39722 + poetry run uvicorn main:app --host 0.0.0.0 --port 39722 --reload ``` ### **코드 컨벤션 (PEP 8, Ruff, Black)** From c63189a107013f6d612191a47fa5f97b237e7b92 Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 21:31:04 +0900 Subject: [PATCH 22/34] =?UTF-8?q?refactor:=20connect=5Fdriver=20router=20-?= =?UTF-8?q?>=20api=5Frouter=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_router.py | 5 +++-- app/main.py | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/api/api_router.py b/app/api/api_router.py index 4c92561..f633400 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -1,10 +1,11 @@ from fastapi import APIRouter -from app.api import test_api +from app.api import connect_driver, test_api + api_router = APIRouter() # 테스트 라우터 api_router.include_router(test_api.router, prefix="/test", tags=["Test"]) # 라우터 -# api_router.include_router(connect_driver.router, prefix="/connections", tags=["Driver"]) \ No newline at end of file +api_router.include_router(connect_driver.router, prefix="/connections", tags=["Driver"]) diff --git a/app/main.py b/app/main.py index 831928a..c3d6dd2 100644 --- a/app/main.py +++ b/app/main.py @@ -3,10 +3,7 @@ import uvicorn from fastapi import FastAPI -from app.api import ( - connect_driver, # 드라이버 확인 - health, # 헬스 체크 -) +from app.api import health # 헬스 체크 from app.api.api_router import api_router from app.core.exceptions import APIException, api_exception_handler, generic_exception_handler @@ -19,7 +16,6 @@ # 라우터 app.include_router(health.router) app.include_router(api_router, prefix="/api") -app.include_router(connect_driver.router, prefix="/api") if __name__ == "__main__": From 4c3b761e6f21a76afd7fd0424cdaf126f1bc49db Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 21:32:17 +0900 Subject: [PATCH 23/34] =?UTF-8?q?feat:=20=20422=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/status.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/core/status.py b/app/core/status.py index 790717d..7803aa4 100644 --- a/app/core/status.py +++ b/app/core/status.py @@ -1,6 +1,8 @@ from enum import Enum + from fastapi import status + class CommonCode(Enum): """ 애플리케이션의 모든 상태 코드를 중앙에서 관리합니다. @@ -21,13 +23,13 @@ class CommonCode(Enum): NO_VALUE = (status.HTTP_400_BAD_REQUEST, "4000", "필수 값이 존재하지 않습니다.") DUPLICATION = (status.HTTP_409_CONFLICT, "4001", "이미 존재하는 데이터입니다.") NO_SEARCH_DATA = (status.HTTP_404_NOT_FOUND, "4002", "요청한 데이터를 찾을 수 없습니다.") + INVALID_ENUM_VALUE = (status.HTTP_422_UNPROCESSABLE_ENTITY, "4003", "유효하지 않은 열거형 값입니다.") # ================================== # 서버 오류 (Server Error) - 5xx # ================================== FAIL = (status.HTTP_500_INTERNAL_SERVER_ERROR, "9999", "서버 처리 중 오류가 발생했습니다.") - def __init__(self, http_status: int, code: str, message: str): """Enum 멤버가 생성될 때 각 값을 속성으로 할당합니다.""" self.http_status = http_status @@ -39,4 +41,3 @@ def get_message(self, *args) -> str: 메시지 포맷팅이 필요한 경우, 인자를 받아 완성된 메시지를 반환합니다. """ return self.message % args if args else self.message - From e3fbb4b29f617984e651c98b6b19d7d4c645f4f9 Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 21:32:58 +0900 Subject: [PATCH 24/34] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connect_driver.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/api/connect_driver.py b/app/api/connect_driver.py index d2e1ff8..f5f8495 100644 --- a/app/api/connect_driver.py +++ b/app/api/connect_driver.py @@ -2,18 +2,22 @@ from fastapi import APIRouter -from app.schemas.driver_info import DriverEnum, DriverInfoResponse +from app.assets.driver_enum import DriverEnum +from app.core.exceptions import APIException +from app.core.status import CommonCode +from app.schemas.driver_info import DriverInfo +from app.schemas.response import ResponseMessage from app.services.driver_info_provider import db_driver_info -router = APIRouter(tags=["Driver"]) +router = APIRouter() -@router.get( - "/connections/drivers/{driver_id}", - summary="DB 드라이버 정보 조회 API", - response_model=DriverInfoResponse, -) -def read_driver_info(driver_id: DriverEnum): - module = driver_id.driver_module - db_type = driver_id.value - return db_driver_info(db_type, module) +@router.get("/drivers/{driverId}", response_model=ResponseMessage[DriverInfo], summary="DB 드라이버 정보 조회 API") +def read_driver_info(driverId: str): + """DB 드라이버 정보 조회""" + for driver in DriverEnum: + if driver.db_type == driverId: + return ResponseMessage.success(value=db_driver_info(driver.db_type, driver.driver_module)) + + # db_type 초기 유효성 검사 실패시 + raise APIException(CommonCode.INVALID_ENUM_VALUE) From a5d622ab591ba182b92d10a08ac3900392b074eb Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 21:34:35 +0900 Subject: [PATCH 25/34] =?UTF-8?q?feat:=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=95=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A8=EB=8D=B8=20=ED=8C=A9=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?Enum=20=EB=B3=80=EC=88=98->=20=EA=B0=9D=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/driver_enum.py | 20 ++++++++++++ app/schemas/driver_info.py | 49 +++++++++++----------------- app/services/driver_info_provider.py | 29 +++++----------- 3 files changed, 47 insertions(+), 51 deletions(-) create mode 100644 app/assets/driver_enum.py diff --git a/app/assets/driver_enum.py b/app/assets/driver_enum.py new file mode 100644 index 0000000..4560ff3 --- /dev/null +++ b/app/assets/driver_enum.py @@ -0,0 +1,20 @@ +# app/assets/driver_enum.py +from enum import Enum + + +class DriverEnum(Enum): + """지원되는 데이터베이스 드라이버 타입""" + + postgresql = ("postgresql", "psycopg2") + mysql = ("mysql", "mysql.connector") + sqlite = ("sqlite", "sqlite3") + oracle = ("oracle", "cx_Oracle") + sqlserver = ("sqlserver", "pyodbc") + mariadb = ("mariadb", "pymysql") + + def __init__(self, db_type, driver_module): + self.db_type = db_type + self.driver_module = driver_module + + def __str__(self): + return self.db_type diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py index 5478ba4..e32f96c 100644 --- a/app/schemas/driver_info.py +++ b/app/schemas/driver_info.py @@ -1,24 +1,10 @@ -# app/schemas/driver_schemas.py -from enum import Enum +# app/schemas/driver_info.py +import importlib +import os from pydantic import BaseModel -class DriverEnum(str, Enum): - postgresql = ("postgresql", "psycopg2") - mysql = ("mysql", "mysql.connector") - sqlite = ("sqlite", "sqlite3") - oracle = ("oracle", "cx_Oracle") - sqlserver = ("sqlserver", "pyodbc") - mariadb = ("mariadb", "pymysql") - - def __new__(cls, db_type, module): - obj = str.__new__(cls, db_type) - obj._value_ = db_type # 초기 유효성 검사 값 - obj.driver_module = module # db_type에 맞는 드라이버 - return obj - - class DriverInfo(BaseModel): db_type: str is_installed: bool @@ -26,17 +12,20 @@ class DriverInfo(BaseModel): driver_version: str | None driver_size_bytes: int | None - -class DriverInfoResponse(BaseModel): - message: str - data: DriverInfo | None = None - - @classmethod - def success(cls, value: DriverInfo): - return cls(message="드라이버 정보를 성공적으로 불러왔습니다.", data=value) - @classmethod - def error(cls, e: Exception | None = None): - if e: - print(f"[ERROR] {type(e).__name__}: {e}") # 로그 - return cls(message="드라이버 정보를 가져오지 못했습니다. 다시 시도해주세요.", data=None) + def from_module(cls, db_type: str, module_name: str): + """모듈 이름으로부터 DriverInfo 객체를 생성하는 팩토리 메서드""" + # 서비스에 있던 로직을 이곳으로 이동 + mod = importlib.import_module(module_name) + version = getattr(mod, "__version__", None) + path = getattr(mod.__spec__, "origin", None) + size = os.path.getsize(path) if path else None + + # 자기 자신의 인스턴스를 생성하여 반환 + return cls( + db_type=db_type, + is_installed=True, + driver_name=module_name, + driver_version=version, + driver_size_bytes=size, + ) diff --git a/app/services/driver_info_provider.py b/app/services/driver_info_provider.py index f702735..cf6e13a 100644 --- a/app/services/driver_info_provider.py +++ b/app/services/driver_info_provider.py @@ -1,26 +1,13 @@ # app/service/driver_info_provider.py +from app.core.exceptions import APIException +from app.core.status import CommonCode +from app.schemas.driver_info import DriverInfo -import importlib -import os -from app.schemas.driver_info import DriverInfo, DriverInfoResponse - - -def db_driver_info(db_type: str, module_name: str) -> DriverInfoResponse: +def db_driver_info(db_type: str, module_name: str): try: - mod = importlib.import_module(module_name) - version = getattr(mod, "__version__", None) - path = getattr(mod.__spec__, "origin", None) - size = os.path.getsize(path) if path else None - - info = DriverInfo( - db_type=db_type, - is_installed=True, - driver_name=module_name, - driver_version=version, - driver_size_bytes=size, - ) - return DriverInfoResponse.success(info) + info = DriverInfo.from_module(db_type, module_name) + return info - except (ModuleNotFoundError, AttributeError, OSError) as e: - return DriverInfoResponse.error(e) + except (ModuleNotFoundError, AttributeError, OSError): + raise APIException(CommonCode.FAIL) From 4b3927b9a54699284ab48fec955f2e88b99717f8 Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 23:34:33 +0900 Subject: [PATCH 26/34] =?UTF-8?q?refactor:=20driver=5Fenum.py=20/core=20?= =?UTF-8?q?=EC=95=84=EB=9E=98=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connect_driver.py | 2 +- app/{assets => core}/driver_enum.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/{assets => core}/driver_enum.py (100%) diff --git a/app/api/connect_driver.py b/app/api/connect_driver.py index f5f8495..aba4958 100644 --- a/app/api/connect_driver.py +++ b/app/api/connect_driver.py @@ -2,7 +2,7 @@ from fastapi import APIRouter -from app.assets.driver_enum import DriverEnum +from app.core.driver_enum import DriverEnum from app.core.exceptions import APIException from app.core.status import CommonCode from app.schemas.driver_info import DriverInfo diff --git a/app/assets/driver_enum.py b/app/core/driver_enum.py similarity index 100% rename from app/assets/driver_enum.py rename to app/core/driver_enum.py From 5fa935869e694e0c6446a53b6e229be6a3134c4f Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 23:34:55 +0900 Subject: [PATCH 27/34] =?UTF-8?q?refactor:=20422=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EC=A2=81=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/status.py b/app/core/status.py index 7803aa4..bd52104 100644 --- a/app/core/status.py +++ b/app/core/status.py @@ -23,7 +23,7 @@ class CommonCode(Enum): NO_VALUE = (status.HTTP_400_BAD_REQUEST, "4000", "필수 값이 존재하지 않습니다.") DUPLICATION = (status.HTTP_409_CONFLICT, "4001", "이미 존재하는 데이터입니다.") NO_SEARCH_DATA = (status.HTTP_404_NOT_FOUND, "4002", "요청한 데이터를 찾을 수 없습니다.") - INVALID_ENUM_VALUE = (status.HTTP_422_UNPROCESSABLE_ENTITY, "4003", "유효하지 않은 열거형 값입니다.") + INVALID_ENUM_VALUE = (status.HTTP_422_UNPROCESSABLE_ENTITY, "4003", "지원하지 않는 데이터베이스 값입니다.") # ================================== # 서버 오류 (Server Error) - 5xx From 2250308107d32f4752274d7a7a46c7ee85bf7426 Mon Sep 17 00:00:00 2001 From: mini Date: Sat, 2 Aug 2025 23:35:31 +0900 Subject: [PATCH 28/34] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=EB=8B=B4?= =?UTF-8?q?=EC=95=84=EC=84=9C=20=EB=B0=98=ED=99=98=ED=96=88=EB=8D=98=20?= =?UTF-8?q?=EA=B1=B8=20=EB=B3=80=EC=88=98=20=EC=A0=9C=EA=B1=B0=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/driver_info_provider.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/services/driver_info_provider.py b/app/services/driver_info_provider.py index cf6e13a..63905da 100644 --- a/app/services/driver_info_provider.py +++ b/app/services/driver_info_provider.py @@ -6,8 +6,7 @@ def db_driver_info(db_type: str, module_name: str): try: - info = DriverInfo.from_module(db_type, module_name) - return info + return DriverInfo.from_module(db_type, module_name) except (ModuleNotFoundError, AttributeError, OSError): raise APIException(CommonCode.FAIL) From a988e61b1def041db3c13e44bf57f67794e91dea Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 3 Aug 2025 17:50:33 +0900 Subject: [PATCH 29/34] =?UTF-8?q?feat:=20=EB=93=9C=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=20=EC=A0=95=EB=B3=B4=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connect_driver.py | 16 ++++++++++------ app/schemas/driver_info.py | 28 +++++++++++++++------------- app/services/driver_info_provider.py | 12 ++++++++++-- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/app/api/connect_driver.py b/app/api/connect_driver.py index aba4958..815d2a8 100644 --- a/app/api/connect_driver.py +++ b/app/api/connect_driver.py @@ -2,7 +2,7 @@ from fastapi import APIRouter -from app.core.driver_enum import DriverEnum +from app.core.db_driver_enum import DBTypesEnum from app.core.exceptions import APIException from app.core.status import CommonCode from app.schemas.driver_info import DriverInfo @@ -15,9 +15,13 @@ @router.get("/drivers/{driverId}", response_model=ResponseMessage[DriverInfo], summary="DB 드라이버 정보 조회 API") def read_driver_info(driverId: str): """DB 드라이버 정보 조회""" - for driver in DriverEnum: - if driver.db_type == driverId: - return ResponseMessage.success(value=db_driver_info(driver.db_type, driver.driver_module)) + try: + # DBTypesEnum 객체를 한 줄로 가져옵니다. + db_type_enum = DBTypesEnum[driverId.lower()] - # db_type 초기 유효성 검사 실패시 - raise APIException(CommonCode.INVALID_ENUM_VALUE) + return ResponseMessage.success( + value=db_driver_info(DriverInfo.from_driver_info(db_type=db_type_enum.name, driver_name=db_type_enum.value)) + ) + # db_type 유효성 검사 실패시 + except KeyError: + raise APIException(CommonCode.INVALID_ENUM_VALUE) diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py index e32f96c..ca0936c 100644 --- a/app/schemas/driver_info.py +++ b/app/schemas/driver_info.py @@ -1,7 +1,4 @@ # app/schemas/driver_info.py -import importlib -import os - from pydantic import BaseModel @@ -13,19 +10,24 @@ class DriverInfo(BaseModel): driver_size_bytes: int | None @classmethod - def from_module(cls, db_type: str, module_name: str): - """모듈 이름으로부터 DriverInfo 객체를 생성하는 팩토리 메서드""" - # 서비스에 있던 로직을 이곳으로 이동 - mod = importlib.import_module(module_name) - version = getattr(mod, "__version__", None) - path = getattr(mod.__spec__, "origin", None) - size = os.path.getsize(path) if path else None - - # 자기 자신의 인스턴스를 생성하여 반환 + def from_module( + cls, db_type: str, driver_name: str, version: str | None, size: int | None + ): # 자기 자신의 인스턴스를 생성하여 반환 return cls( db_type=db_type, is_installed=True, - driver_name=module_name, + driver_name=driver_name, driver_version=version, driver_size_bytes=size, ) + + @classmethod + def from_driver_info(cls, db_type: str, driver_name: str): + # 최소한의 정보로 객체를 생성할 때 사용 + return cls( + db_type=db_type, + is_installed=False, + driver_name=driver_name, + driver_version=None, + driver_size_bytes=None, + ) diff --git a/app/services/driver_info_provider.py b/app/services/driver_info_provider.py index 63905da..f2ffcd1 100644 --- a/app/services/driver_info_provider.py +++ b/app/services/driver_info_provider.py @@ -1,12 +1,20 @@ # app/service/driver_info_provider.py +import importlib +import os + from app.core.exceptions import APIException from app.core.status import CommonCode from app.schemas.driver_info import DriverInfo -def db_driver_info(db_type: str, module_name: str): +def db_driver_info(driver_info: DriverInfo): try: - return DriverInfo.from_module(db_type, module_name) + mod = importlib.import_module(driver_info.driver_name) + version = getattr(mod, "__version__", None) + path = getattr(mod.__spec__, "origin", None) + size = os.path.getsize(path) if path else None + + return DriverInfo.from_module(driver_info.db_type, driver_info.driver_name, version, size) except (ModuleNotFoundError, AttributeError, OSError): raise APIException(CommonCode.FAIL) From 21bf04f0395c14004fc760f7e1bc0337a8e59606 Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 3 Aug 2025 17:52:29 +0900 Subject: [PATCH 30/34] =?UTF-8?q?refactor:=20DB=20=EB=93=9C=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B2=84=20Enum=20=EA=B0=92=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/db_driver_enum.py | 13 +++++++++++++ app/core/driver_enum.py | 20 -------------------- 2 files changed, 13 insertions(+), 20 deletions(-) create mode 100644 app/core/db_driver_enum.py delete mode 100644 app/core/driver_enum.py diff --git a/app/core/db_driver_enum.py b/app/core/db_driver_enum.py new file mode 100644 index 0000000..6896760 --- /dev/null +++ b/app/core/db_driver_enum.py @@ -0,0 +1,13 @@ +# app/core/db_driver_enum.py +from enum import Enum + + +class DBTypesEnum(Enum): + """지원되는 데이터베이스 드라이버 타입""" + + postgresql = "psycopg2" + mysql = "mysql.connector" + sqlite = "sqlite3" + oracle = "cx_Oracle" + sqlserver = "pyodbc" + mariadb = "pymysql" diff --git a/app/core/driver_enum.py b/app/core/driver_enum.py deleted file mode 100644 index 4560ff3..0000000 --- a/app/core/driver_enum.py +++ /dev/null @@ -1,20 +0,0 @@ -# app/assets/driver_enum.py -from enum import Enum - - -class DriverEnum(Enum): - """지원되는 데이터베이스 드라이버 타입""" - - postgresql = ("postgresql", "psycopg2") - mysql = ("mysql", "mysql.connector") - sqlite = ("sqlite", "sqlite3") - oracle = ("oracle", "cx_Oracle") - sqlserver = ("sqlserver", "pyodbc") - mariadb = ("mariadb", "pymysql") - - def __init__(self, db_type, driver_module): - self.db_type = db_type - self.driver_module = driver_module - - def __str__(self): - return self.db_type From 6120bc2d0f6b7d798e53659d2a0be639fcae087b Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 3 Aug 2025 21:40:12 +0900 Subject: [PATCH 31/34] =?UTF-8?q?refactor:=20=EB=9D=BC=EC=9A=B0=ED=84=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20from=5Fenum=20=ED=8C=A9=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/connect_driver.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/api/connect_driver.py b/app/api/connect_driver.py index 815d2a8..bc9b132 100644 --- a/app/api/connect_driver.py +++ b/app/api/connect_driver.py @@ -18,10 +18,7 @@ def read_driver_info(driverId: str): try: # DBTypesEnum 객체를 한 줄로 가져옵니다. db_type_enum = DBTypesEnum[driverId.lower()] - - return ResponseMessage.success( - value=db_driver_info(DriverInfo.from_driver_info(db_type=db_type_enum.name, driver_name=db_type_enum.value)) - ) + return ResponseMessage.success(value=db_driver_info(DriverInfo.from_enum(db_type_enum))) # db_type 유효성 검사 실패시 except KeyError: raise APIException(CommonCode.INVALID_ENUM_VALUE) From aa7d8c1d46c4b945bfd02d620f514d80534be4aa Mon Sep 17 00:00:00 2001 From: mini Date: Sun, 3 Aug 2025 21:40:36 +0900 Subject: [PATCH 32/34] =?UTF-8?q?feat:=20from=5Fenum=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/driver_info.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py index ca0936c..7447330 100644 --- a/app/schemas/driver_info.py +++ b/app/schemas/driver_info.py @@ -1,6 +1,8 @@ # app/schemas/driver_info.py from pydantic import BaseModel +from app.core.db_driver_enum import DBTypesEnum + class DriverInfo(BaseModel): db_type: str @@ -10,9 +12,11 @@ class DriverInfo(BaseModel): driver_size_bytes: int | None @classmethod - def from_module( - cls, db_type: str, driver_name: str, version: str | None, size: int | None - ): # 자기 자신의 인스턴스를 생성하여 반환 + def from_module(cls, db_type: str, driver_name: str, version: str | None, size: int | None): + """ + 설치된 드라이버의 모든 정보를 바탕으로 DriverInfo 객체를 생성합니다. + `is_installed`는 항상 True로 설정됩니다. + """ return cls( db_type=db_type, is_installed=True, @@ -23,7 +27,10 @@ def from_module( @classmethod def from_driver_info(cls, db_type: str, driver_name: str): - # 최소한의 정보로 객체를 생성할 때 사용 + """ + 최소한의 정보(DB 타입, 드라이버 이름)만으로 초기 DriverInfo 객체를 생성합니다. + `is_installed`는 False로 설정됩니다. + """ return cls( db_type=db_type, is_installed=False, @@ -31,3 +38,10 @@ def from_driver_info(cls, db_type: str, driver_name: str): driver_version=None, driver_size_bytes=None, ) + + @classmethod + def from_enum(cls, db_type_enum: DBTypesEnum): + """ + DBTypesEnum 객체를 인자로 받아, from_driver_info를 호출해 초기 객체를 생성합니다. + """ + return cls.from_driver_info(db_type=db_type_enum.name, driver_name=db_type_enum.value) From 32525412af41223769c6f8c62c6e7cd154cedcaf Mon Sep 17 00:00:00 2001 From: mini Date: Mon, 4 Aug 2025 00:09:15 +0900 Subject: [PATCH 33/34] =?UTF-8?q?refactor:=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=83=88=EC=83=9D=EC=84=B1=EC=9D=B4=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EA=B0=9D=EC=B2=B4=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20api,=20service=20=EC=9D=98?= =?UTF-8?q?=EB=AF=B8=20=EB=8F=99=EC=9D=BC=ED=95=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_router.py | 4 +-- app/api/{connect_driver.py => driver_api.py} | 8 ++--- app/schemas/driver_info.py | 29 ++++++----------- ...ver_info_provider.py => driver_service.py} | 5 +-- app/tests/api/test_driver_info_api.py | 31 ------------------ app/tests/unit/test_driver_info_unit.py | 32 ------------------- 6 files changed, 18 insertions(+), 91 deletions(-) rename app/api/{connect_driver.py => driver_api.py} (78%) rename app/services/{driver_info_provider.py => driver_service.py} (79%) delete mode 100644 app/tests/api/test_driver_info_api.py delete mode 100644 app/tests/unit/test_driver_info_unit.py diff --git a/app/api/api_router.py b/app/api/api_router.py index f633400..1067cdc 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api import connect_driver, test_api +from app.api import driver_api, test_api api_router = APIRouter() @@ -8,4 +8,4 @@ api_router.include_router(test_api.router, prefix="/test", tags=["Test"]) # 라우터 -api_router.include_router(connect_driver.router, prefix="/connections", tags=["Driver"]) +api_router.include_router(driver_api.router, prefix="/connections", tags=["Driver"]) diff --git a/app/api/connect_driver.py b/app/api/driver_api.py similarity index 78% rename from app/api/connect_driver.py rename to app/api/driver_api.py index bc9b132..97a9e10 100644 --- a/app/api/connect_driver.py +++ b/app/api/driver_api.py @@ -1,4 +1,4 @@ -# app/api/connect_driver.py +# app/api/driver_api.py from fastapi import APIRouter @@ -7,7 +7,7 @@ from app.core.status import CommonCode from app.schemas.driver_info import DriverInfo from app.schemas.response import ResponseMessage -from app.services.driver_info_provider import db_driver_info +from app.services.driver_service import db_driver_info router = APIRouter() @@ -16,9 +16,9 @@ def read_driver_info(driverId: str): """DB 드라이버 정보 조회""" try: - # DBTypesEnum 객체를 한 줄로 가져옵니다. + # DBTypesEnum에서 driverID에 맞는 객체를 가져옵니다. db_type_enum = DBTypesEnum[driverId.lower()] return ResponseMessage.success(value=db_driver_info(DriverInfo.from_enum(db_type_enum))) - # db_type 유효성 검사 실패시 + # db_type_enum 유효성 검사 실패 except KeyError: raise APIException(CommonCode.INVALID_ENUM_VALUE) diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py index 7447330..1fa41ca 100644 --- a/app/schemas/driver_info.py +++ b/app/schemas/driver_info.py @@ -11,26 +11,22 @@ class DriverInfo(BaseModel): driver_version: str | None driver_size_bytes: int | None - @classmethod - def from_module(cls, db_type: str, driver_name: str, version: str | None, size: int | None): + def update_from_module(self, version: str | None, size: int | None): """ - 설치된 드라이버의 모든 정보를 바탕으로 DriverInfo 객체를 생성합니다. - `is_installed`는 항상 True로 설정됩니다. + 객체 자신의 속성을 직접 업데이트하여 설치된 드라이버 정보를 채웁니다. """ - return cls( - db_type=db_type, - is_installed=True, - driver_name=driver_name, - driver_version=version, - driver_size_bytes=size, - ) + self.is_installed = True + self.driver_version = version + self.driver_size_bytes = size @classmethod - def from_driver_info(cls, db_type: str, driver_name: str): + def from_enum(cls, db_type_enum: DBTypesEnum): """ - 최소한의 정보(DB 타입, 드라이버 이름)만으로 초기 DriverInfo 객체를 생성합니다. + DBTypesEnum 객체를 인자로 받아, db_type, driver_name만으로 driverInfo 객체를 생성합니다. `is_installed`는 False로 설정됩니다. """ + db_type = db_type_enum.name + driver_name = db_type_enum.value return cls( db_type=db_type, is_installed=False, @@ -38,10 +34,3 @@ def from_driver_info(cls, db_type: str, driver_name: str): driver_version=None, driver_size_bytes=None, ) - - @classmethod - def from_enum(cls, db_type_enum: DBTypesEnum): - """ - DBTypesEnum 객체를 인자로 받아, from_driver_info를 호출해 초기 객체를 생성합니다. - """ - return cls.from_driver_info(db_type=db_type_enum.name, driver_name=db_type_enum.value) diff --git a/app/services/driver_info_provider.py b/app/services/driver_service.py similarity index 79% rename from app/services/driver_info_provider.py rename to app/services/driver_service.py index f2ffcd1..9f35092 100644 --- a/app/services/driver_info_provider.py +++ b/app/services/driver_service.py @@ -1,4 +1,4 @@ -# app/service/driver_info_provider.py +# app/service/driver_service.py import importlib import os @@ -14,7 +14,8 @@ def db_driver_info(driver_info: DriverInfo): path = getattr(mod.__spec__, "origin", None) size = os.path.getsize(path) if path else None - return DriverInfo.from_module(driver_info.db_type, driver_info.driver_name, version, size) + driver_info.update_from_module(version, size) + return driver_info except (ModuleNotFoundError, AttributeError, OSError): raise APIException(CommonCode.FAIL) diff --git a/app/tests/api/test_driver_info_api.py b/app/tests/api/test_driver_info_api.py deleted file mode 100644 index f64083a..0000000 --- a/app/tests/api/test_driver_info_api.py +++ /dev/null @@ -1,31 +0,0 @@ -from fastapi.testclient import TestClient - -from app.main import app # FastAPI 앱 객체 - -client = TestClient(app) - - -def test_api_supported_driver(): - response = client.get("api/connections/drivers/mysql") - assert response.status_code == 200 - - body = response.json() - assert body["message"] == "드라이버 정보를 성공적으로 불러왔습니다." - assert body["data"]["db_type"] == "mysql" - assert body["data"]["is_installed"] is True - assert body["data"]["driver_name"] == "mysql.connector" - assert isinstance(body["data"]["driver_size_bytes"], int) - - -def test_api_unsupported_driver(): - response = client.get("api/connections/drivers/unknown-db") - assert response.status_code == 422 # ❗ Enum validation이 터짐 - - body = response.json() - assert body["detail"][0]["msg"].startswith("Input should be") - assert body["detail"][0]["loc"] == ["path", "driver_id"] - - -def test_api_empty_driver(): - response = client.get("api/connections/drivers/") # 누락된 경로 - assert response.status_code in [404, 422] diff --git a/app/tests/unit/test_driver_info_unit.py b/app/tests/unit/test_driver_info_unit.py deleted file mode 100644 index 93b53bd..0000000 --- a/app/tests/unit/test_driver_info_unit.py +++ /dev/null @@ -1,32 +0,0 @@ -from unittest import mock - -from app.api.connect_driver import DriverEnum -from app.services.driver_info_provider import db_driver_info - - -# 드라이버 설치 되었을 때 -def test_supported_driver_installed(): - driver = DriverEnum.mysql - result = db_driver_info(driver.value, driver.driver_module) - print(result) - - -# 존재하지 않는 DB 타입을 넘겼을 때 (Enum 변환 실패) -def test_invalid_enum_value(): - try: - DriverEnum("nonexistent-driver") - assert False # 실패하지 않으면 오류! - except ValueError as e: - print(f"[Enum 변환 실패] {e}") # 콘솔에 예외 메시지 출력 - assert True - - -# 빈 값 넘겼을 때 -> 제거: Enum 유효성 검사로 인해 필요성 없음 - - -# importlib 실패 상황 테스트 -def test_driver_import_failure_at_api(): - with mock.patch("importlib.import_module", side_effect=ModuleNotFoundError("모듈 없음")): - driver = DriverEnum.mysql - result = db_driver_info(driver.value, driver.driver_module) - print(result) From c1fc742eafa6aee7e014a623dad08b15312282e6 Mon Sep 17 00:00:00 2001 From: mini Date: Mon, 4 Aug 2025 00:16:28 +0900 Subject: [PATCH 34/34] =?UTF-8?q?refactor:=20driver=5Finfo=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20return=20=EB=88=84=EB=9D=BD=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/schemas/driver_info.py | 2 ++ app/services/driver_service.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/schemas/driver_info.py b/app/schemas/driver_info.py index 1fa41ca..f8e0b62 100644 --- a/app/schemas/driver_info.py +++ b/app/schemas/driver_info.py @@ -19,6 +19,8 @@ def update_from_module(self, version: str | None, size: int | None): self.driver_version = version self.driver_size_bytes = size + return self + @classmethod def from_enum(cls, db_type_enum: DBTypesEnum): """ diff --git a/app/services/driver_service.py b/app/services/driver_service.py index 9f35092..afed25c 100644 --- a/app/services/driver_service.py +++ b/app/services/driver_service.py @@ -14,8 +14,7 @@ def db_driver_info(driver_info: DriverInfo): path = getattr(mod.__spec__, "origin", None) size = os.path.getsize(path) if path else None - driver_info.update_from_module(version, size) - return driver_info + return driver_info.update_from_module(version, size) except (ModuleNotFoundError, AttributeError, OSError): raise APIException(CommonCode.FAIL)