├── .circleci └── config.yml ├── .coveragerc ├── .gitignore ├── LICENSE ├── MENIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── deploy.sh ├── iamporter ├── __init__.py ├── api.py ├── base.py ├── client.py ├── consts.py └── errors.py ├── setup.cfg ├── setup.py └── test.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` 11 | - image: cimg/python:3.8 12 | 13 | # Specify service dependencies here if necessary 14 | # CircleCI maintains a library of pre-built images 15 | # documented at https://circleci.com/docs/2.0/circleci-images/ 16 | # - image: circleci/postgres:9.4 17 | 18 | working_directory: ~/repo 19 | 20 | steps: 21 | - checkout 22 | 23 | # Download and cache dependencies 24 | - restore_cache: 25 | keys: 26 | - v1-dependencies-{{ checksum "Pipfile" }} 27 | # fallback to using the latest cache if no exact match is found 28 | - v1-dependencies- 29 | 30 | - run: 31 | name: install dependencies 32 | command: | 33 | pip3 install pipenv 34 | pipenv install --dev 35 | 36 | - save_cache: 37 | paths: 38 | - ./venv 39 | key: v1-dependencies-{{ checksum "Pipfile" }} 40 | 41 | - run: 42 | name: run tests 43 | command: | 44 | pipenv run coverage run ./test.py 45 | pipenv run codecov 46 | 47 | - store_artifacts: 48 | path: test-reports 49 | destination: test-reports -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = iamporter 4 | omit = 5 | venv/* 6 | setup.py -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/macos,linux,python,windows,pycharm+all 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | ### PyCharm+all ### 48 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 49 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 50 | 51 | # User-specific stuff 52 | .idea/**/workspace.xml 53 | .idea/**/tasks.xml 54 | .idea/**/usage.statistics.xml 55 | .idea/**/dictionaries 56 | .idea/**/shelf 57 | 58 | # Sensitive or high-churn files 59 | .idea/**/dataSources/ 60 | .idea/**/dataSources.ids 61 | .idea/**/dataSources.local.xml 62 | .idea/**/sqlDataSources.xml 63 | .idea/**/dynamic.xml 64 | .idea/**/uiDesigner.xml 65 | .idea/**/dbnavigator.xml 66 | 67 | # Gradle 68 | .idea/**/gradle.xml 69 | .idea/**/libraries 70 | 71 | # Gradle and Maven with auto-import 72 | # When using Gradle or Maven with auto-import, you should exclude module files, 73 | # since they will be recreated, and may cause churn. Uncomment if using 74 | # auto-import. 75 | # .idea/modules.xml 76 | # .idea/*.iml 77 | # .idea/modules 78 | 79 | # CMake 80 | cmake-build-*/ 81 | 82 | # Mongo Explorer plugin 83 | .idea/**/mongoSettings.xml 84 | 85 | # File-based project format 86 | *.iws 87 | 88 | # IntelliJ 89 | out/ 90 | 91 | # mpeltonen/sbt-idea plugin 92 | .idea_modules/ 93 | 94 | # JIRA plugin 95 | atlassian-ide-plugin.xml 96 | 97 | # Cursive Clojure plugin 98 | .idea/replstate.xml 99 | 100 | # Crashlytics plugin (for Android Studio and IntelliJ) 101 | com_crashlytics_export_strings.xml 102 | crashlytics.properties 103 | crashlytics-build.properties 104 | fabric.properties 105 | 106 | # Editor-based Rest Client 107 | .idea/httpRequests 108 | 109 | ### PyCharm+all Patch ### 110 | # Ignores the whole .idea folder and all .iml files 111 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 112 | 113 | .idea/ 114 | 115 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 116 | 117 | *.iml 118 | modules.xml 119 | .idea/misc.xml 120 | *.ipr 121 | 122 | ### Python ### 123 | # Byte-compiled / optimized / DLL files 124 | __pycache__/ 125 | *.py[cod] 126 | *$py.class 127 | 128 | # C extensions 129 | *.so 130 | 131 | # Distribution / packaging 132 | .Python 133 | build/ 134 | develop-eggs/ 135 | dist/ 136 | downloads/ 137 | eggs/ 138 | .eggs/ 139 | lib/ 140 | lib64/ 141 | parts/ 142 | sdist/ 143 | var/ 144 | wheels/ 145 | *.egg-info/ 146 | .installed.cfg 147 | *.egg 148 | MANIFEST 149 | 150 | # PyInstaller 151 | # Usually these files are written by a python script from a template 152 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 153 | *.manifest 154 | *.spec 155 | 156 | # Installer logs 157 | pip-log.txt 158 | pip-delete-this-directory.txt 159 | 160 | # Unit test / coverage reports 161 | htmlcov/ 162 | .tox/ 163 | .coverage 164 | .coverage.* 165 | .cache 166 | nosetests.xml 167 | coverage.xml 168 | *.cover 169 | .hypothesis/ 170 | .pytest_cache/ 171 | 172 | # Translations 173 | *.mo 174 | *.pot 175 | 176 | # Django stuff: 177 | *.log 178 | local_settings.py 179 | db.sqlite3 180 | 181 | # Flask stuff: 182 | instance/ 183 | .webassets-cache 184 | 185 | # Scrapy stuff: 186 | .scrapy 187 | 188 | # Sphinx documentation 189 | docs/_build/ 190 | 191 | # PyBuilder 192 | target/ 193 | 194 | # Jupyter Notebook 195 | .ipynb_checkpoints 196 | 197 | # pyenv 198 | .python-version 199 | 200 | # celery beat schedule file 201 | celerybeat-schedule 202 | 203 | # SageMath parsed files 204 | *.sage.py 205 | 206 | # Environments 207 | .env 208 | .venv 209 | env/ 210 | venv/ 211 | ENV/ 212 | env.bak/ 213 | venv.bak/ 214 | 215 | # Spyder project settings 216 | .spyderproject 217 | .spyproject 218 | 219 | # Rope project settings 220 | .ropeproject 221 | 222 | # mkdocs documentation 223 | /site 224 | 225 | # mypy 226 | .mypy_cache/ 227 | 228 | ### Python Patch ### 229 | .venv/ 230 | 231 | ### Windows ### 232 | # Windows thumbnail cache files 233 | Thumbs.db 234 | ehthumbs.db 235 | ehthumbs_vista.db 236 | 237 | # Dump file 238 | *.stackdump 239 | 240 | # Folder config file 241 | [Dd]esktop.ini 242 | 243 | # Recycle Bin used on file shares 244 | $RECYCLE.BIN/ 245 | 246 | # Windows Installer files 247 | *.cab 248 | *.msi 249 | *.msix 250 | *.msm 251 | *.msp 252 | 253 | # Windows shortcuts 254 | *.lnk 255 | 256 | 257 | # End of https://www.gitignore.io/api/macos,linux,python,windows,pycharm+all -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DongEon Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MENIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | 3 | include README.md -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [packages] 7 | requests = ">=2.0.0,<3.0.0" 8 | 9 | [dev-packages] 10 | coverage = "*" 11 | codecov = "*" 12 | twine = "*" 13 | 14 | [requires] 15 | python_version = "3.8" 16 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "3bc60b34a3099d8634d61c6d4ebbfe230f8ea1304211a48741c7ff63f6a54957" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 22 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 23 | ], 24 | "version": "==2020.12.5" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 29 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 30 | ], 31 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 32 | "version": "==4.0.0" 33 | }, 34 | "idna": { 35 | "hashes": [ 36 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 37 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 38 | ], 39 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 40 | "version": "==2.10" 41 | }, 42 | "requests": { 43 | "hashes": [ 44 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 45 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 46 | ], 47 | "index": "pypi", 48 | "version": "==2.25.1" 49 | }, 50 | "urllib3": { 51 | "hashes": [ 52 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 53 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 54 | ], 55 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 56 | "version": "==1.26.2" 57 | } 58 | }, 59 | "develop": { 60 | "bleach": { 61 | "hashes": [ 62 | "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", 63 | "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" 64 | ], 65 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 66 | "version": "==3.2.1" 67 | }, 68 | "certifi": { 69 | "hashes": [ 70 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 71 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 72 | ], 73 | "version": "==2020.12.5" 74 | }, 75 | "chardet": { 76 | "hashes": [ 77 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 78 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 79 | ], 80 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 81 | "version": "==4.0.0" 82 | }, 83 | "codecov": { 84 | "hashes": [ 85 | "sha256:6cde272454009d27355f9434f4e49f238c0273b216beda8472a65dc4957f473b", 86 | "sha256:ba8553a82942ce37d4da92b70ffd6d54cf635fc1793ab0a7dc3fecd6ebfb3df8", 87 | "sha256:e95901d4350e99fc39c8353efa450050d2446c55bac91d90fcfd2354e19a6aef" 88 | ], 89 | "index": "pypi", 90 | "version": "==2.1.11" 91 | }, 92 | "colorama": { 93 | "hashes": [ 94 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 95 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 96 | ], 97 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 98 | "version": "==0.4.4" 99 | }, 100 | "coverage": { 101 | "hashes": [ 102 | "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297", 103 | "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1", 104 | "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497", 105 | "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606", 106 | "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528", 107 | "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b", 108 | "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4", 109 | "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830", 110 | "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1", 111 | "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f", 112 | "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d", 113 | "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3", 114 | "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8", 115 | "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500", 116 | "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7", 117 | "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb", 118 | "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b", 119 | "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059", 120 | "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b", 121 | "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72", 122 | "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36", 123 | "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277", 124 | "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c", 125 | "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631", 126 | "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff", 127 | "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8", 128 | "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec", 129 | "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b", 130 | "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7", 131 | "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105", 132 | "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b", 133 | "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c", 134 | "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b", 135 | "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98", 136 | "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4", 137 | "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879", 138 | "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f", 139 | "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4", 140 | "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044", 141 | "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e", 142 | "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899", 143 | "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f", 144 | "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448", 145 | "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714", 146 | "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2", 147 | "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d", 148 | "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd", 149 | "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7", 150 | "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae" 151 | ], 152 | "index": "pypi", 153 | "version": "==5.3.1" 154 | }, 155 | "docutils": { 156 | "hashes": [ 157 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 158 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 159 | ], 160 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 161 | "version": "==0.16" 162 | }, 163 | "idna": { 164 | "hashes": [ 165 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 166 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 167 | ], 168 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 169 | "version": "==2.10" 170 | }, 171 | "keyring": { 172 | "hashes": [ 173 | "sha256:4c41ce4f6d1ee91d589a346699ef5a94ba3429603ac8f700cc0097644cdd6748", 174 | "sha256:a144f7e1044c897c3976202af868cb0ac860f4d433d5d0f8e750fa1a2f0f0b50" 175 | ], 176 | "markers": "python_version >= '3.6'", 177 | "version": "==21.7.0" 178 | }, 179 | "packaging": { 180 | "hashes": [ 181 | "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", 182 | "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093" 183 | ], 184 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 185 | "version": "==20.8" 186 | }, 187 | "pkginfo": { 188 | "hashes": [ 189 | "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", 190 | "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" 191 | ], 192 | "version": "==1.6.1" 193 | }, 194 | "pygments": { 195 | "hashes": [ 196 | "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", 197 | "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08" 198 | ], 199 | "markers": "python_version >= '3.5'", 200 | "version": "==2.7.3" 201 | }, 202 | "pyparsing": { 203 | "hashes": [ 204 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 205 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 206 | ], 207 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 208 | "version": "==2.4.7" 209 | }, 210 | "pywin32-ctypes": { 211 | "hashes": [ 212 | "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", 213 | "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" 214 | ], 215 | "markers": "sys_platform == 'win32'", 216 | "version": "==0.2.0" 217 | }, 218 | "readme-renderer": { 219 | "hashes": [ 220 | "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", 221 | "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" 222 | ], 223 | "version": "==28.0" 224 | }, 225 | "requests": { 226 | "hashes": [ 227 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 228 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 229 | ], 230 | "index": "pypi", 231 | "version": "==2.25.1" 232 | }, 233 | "requests-toolbelt": { 234 | "hashes": [ 235 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 236 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 237 | ], 238 | "version": "==0.9.1" 239 | }, 240 | "rfc3986": { 241 | "hashes": [ 242 | "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", 243 | "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" 244 | ], 245 | "version": "==1.4.0" 246 | }, 247 | "six": { 248 | "hashes": [ 249 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 250 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 251 | ], 252 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 253 | "version": "==1.15.0" 254 | }, 255 | "tqdm": { 256 | "hashes": [ 257 | "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416", 258 | "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208" 259 | ], 260 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 261 | "version": "==4.55.0" 262 | }, 263 | "twine": { 264 | "hashes": [ 265 | "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", 266 | "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" 267 | ], 268 | "index": "pypi", 269 | "version": "==3.3.0" 270 | }, 271 | "urllib3": { 272 | "hashes": [ 273 | "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", 274 | "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" 275 | ], 276 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", 277 | "version": "==1.26.2" 278 | }, 279 | "webencodings": { 280 | "hashes": [ 281 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 282 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 283 | ], 284 | "version": "==0.5.1" 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iamporter-python 2 | 🚀 An I'mport REST API client for Human 3 | 4 | [![CircleCI](https://circleci.com/gh/kde713/iamporter-python.svg?style=svg)](https://circleci.com/gh/kde713/iamporter-python) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/31f5995f6f08494d89c0/maintainability)](https://codeclimate.com/github/kde713/iamporter-python/maintainability) 6 | [![CodeCov](https://img.shields.io/codecov/c/github/kde713/iamporter-python.svg)](https://codecov.io/gh/kde713/iamporter-python) 7 | [![PyPI Version](https://img.shields.io/pypi/v/iamporter.svg)](https://pypi.org/project/iamporter/) 8 | ![License](https://img.shields.io/github/license/kde713/iamporter-python.svg?logo=github) 9 | 10 | **iamporter-python** 는 [아임포트](https://www.iamport.kr/)에서 제공하는 REST API를 쉽게 활용하기 위해 작성된 Python 클라이언트입니다. 11 | 12 | ----- 13 | 14 | ## Why iamporter-python? 15 | 16 | - 실제 프로덕션 서비스에 적용하기 위해 개발된 프로젝트입니다. 17 | - Wrapping한 메소드가 구현되지 않은 경우라도 쉽게 아임포트 API를 요청할 수 있습니다. 18 | - Docstring이 작성되어 있어 PyCharm과 같은 IDE에서 자동완성을 사용할 때 더욱 편리합니다. 19 | 20 | 21 | ## Disclaimer 22 | 23 | - 이용 중 발생한 문제에 대하여 책임을 지지 않습니다. 단, Issue에 `help-wanted` 로 남겨주시면 도움을 드리기 위해 노력하겠습니다. 24 | - Python 2를 지원하지 않습니다. 25 | 26 | 27 | ### Installation 28 | 29 | ```bash 30 | pip install iamporter 31 | ``` 32 | 33 | 34 | 35 | ## Specification 36 | 37 | ##### iamporter.Iamporter 38 | 39 | - **Succeed:** API 응답이 OK인 경우 Response Body 의 response 필드를 `dict` 타입으로 반환합니다. 40 | - **Failed:** HTTP 응답 코드가 403인 경우 `ImpUnAuthorized`, 이외의 경우 `ImpApiError` Exception을 발생시킵니다. 41 | 42 | ##### iamporter.api 43 | 44 | - API 결과와 관계없이 `IamportResponse` 인스턴스를 반환합니다. 45 | 46 | 47 | 48 | ## Usage (General Way) 49 | 50 | REST API를 사용하기 편하게 Wrapping한 Iamporter 객체를 통해 라이브러리를 활용하는 일반적인 방법입니다. 51 | 52 | ### 준비 53 | 54 | 사용하기 위해 객체를 초기화합니다. 55 | `imp_auth` 인자에 `IamportAuth` 인스턴스를 전달하여 객체를 초기화할 수도 있습니다. 56 | (테스트를 위해 `imp_url` 인자에 별도로 구축한 목업 서버 url을 넘겨 초기화할 수도 있습니다. 기본값은 `https://api.iamport.kr/` 입니다.) 57 | 58 | ```python 59 | from iamporter import Iamporter 60 | 61 | client = Iamporter(imp_key="YOUR_IAMPORT_REST_API_KEY", imp_secret="YOUR_IAMPORT_REST_API_SECRET") 62 | ``` 63 | 64 | ### 예외 처리 65 | 66 | - 필수값이 누락된 경우 `KeyError` 예외가 발생합니다. 67 | - 클라이언트 객체 초기화 시 인증정보가 올바르지 않은 경우나 API 응답 HTTP Status Code가 403 인 경우 `iamporter.errors.ImpUnAuthorized` 예외가 발생합니다. 68 | - 이외에 API 응답이 OK가 아닌 경우 `iamporter.errors.ImpApiError` 예외가 발생합니다. 69 | * `.response` 필드에 API 응답이 `IamportResponse` 타입으로 담겨있습니다. 70 | 71 | ### 결제 내역 조회 72 | 73 | 아임포트 고유번호 (`imp_uid`)나 가맹점지정 고유번호 (`merchant_uid`)를 이용해 결제 정보를 조회합니다. 74 | 75 | ```python 76 | client.find_payment(imp_uid="your_imp_uid") 77 | client.find_payment(merchant_uid="your_merchant_uid") 78 | ``` 79 | 80 | ### 결제 취소 81 | 82 | 결제를 취소합니다. 83 | 취소 사유(`reason`), 취소 요청 금액(`amount`), 취소 요청 금액 중 면세금액(`tax_free`) 값을 지정할 수 있습니다. 84 | 85 | ```python 86 | client.cancel_payment(imp_uid="your_imp_uid") 87 | client.cancel_payment(merchant_uid="your_merchant_uid", amount=10000, tax_free=5000) 88 | ``` 89 | 90 | ### 빌링키 발급 91 | 92 | 정기 결제 등에 사용할 수 있는 빌링키를 발급합니다. 93 | PG사 코드(`pg`), 카드소유자 정보(`customer_info`)를 지정할 수 있습니다. 94 | 95 | ```python 96 | client.create_billkey( 97 | customer_uid="your_customer_uid", 98 | card_number="1234-1234-1234-1234", 99 | expiry="2022-06", 100 | birth="960712", 101 | pwd_2digit="12", 102 | customer_info={ 103 | 'name': "소유자 이름", 104 | 'tel': "01000000000", 105 | 'email': "someone@example.com", 106 | 'addr': "사는 곳 주소", 107 | 'postcode': "00000", 108 | }, 109 | ) 110 | ``` 111 | 112 | ### 빌링키 조회 113 | 114 | 빌링키 등록 정보를 조회합니다. 115 | 116 | ```python 117 | client.find_billkey(customer_uid="your_customer_uid") 118 | ``` 119 | 120 | ### 빌링키 삭제 121 | 122 | 빌링키 등록정보를 삭제합니다. 123 | 124 | ```python 125 | client.delete_billkey(customer_uid="your_customer_uid") 126 | ``` 127 | 128 | ### 비인증 결제 요청 129 | 130 | 구매자로 부터 별도의 인증과정을 거치지 않고 신용카드 정보 또는 빌링키를 이용해 결제를 요청합니다. 131 | 카드정보를 지정한 경우 `customer_uid`를 함꼐 지정하면 해당 카드 정보로 결제 후 빌링키가 저장됩니다. 132 | 133 | ```python 134 | from iamporter import Iamporter 135 | client = Iamporter(imp_key="YOUR_IAMPORT_REST_API_KEY", imp_secret="YOUR_IAMPORT_REST_API_SECRET") 136 | client.create_payment( 137 | merchant_uid="your_merchant_uid", 138 | name="주문명", 139 | amount=10000, 140 | card_number="1234-1234-1234-1234", 141 | expiry="2022-06", 142 | birth="960712", 143 | pwd_2digit="12", 144 | buyer_info={ 145 | 'name': "구매자 이름", 146 | 'tel': "01000000000", 147 | 'email': "someone@example.com", 148 | 'addr': "사는 곳 주소", 149 | 'postcode': "00000", 150 | }, 151 | ) 152 | client.create_payment( 153 | merchant_uid="your_merchant_uid", 154 | customer_uid="your_customer_uid", 155 | name="주문명", 156 | amount=10000, 157 | ) 158 | ``` 159 | 160 | > 비인증 해외카드 결제 역시 `.create_payment` 메소드를 사용해주시면 됩니다. 161 | 162 | 163 | 164 | ## Usage (Alternative Way) 165 | 166 | Iamporter 객체에 wrapping 되어 있지 않은 API를 사용하거나, 직접 API-Level에서 개발을 하기 위해 사용하는 방법입니다. 167 | 168 | ### 준비 169 | 170 | 사용하기 위해 인증객체를 만듭니다. 171 | 172 | ```python 173 | from iamporter import IamportAuth 174 | 175 | auth = IamportAuth(imp_key="YOUR_IAMPORT_REST_API_KEY", imp_secret="YOUR_IAMPORT_REST_API_SECRET") 176 | ``` 177 | 178 | ### API Method List 179 | 180 | 모든 API-Level Class 들은 `iamporter.api` 에 위치합니다. 아래는 어떤 방식으로 API 와 대응되는 Class와 method의 이름이 정해지는지에 대한 예입니다. (모든 대응 목록이 아닙니다.) 181 | 182 | | API | Class | Method | 183 | | :-: | :---: | ------ | 184 | | `GET /payments/{imp_uid}/balance` | `Payments` | `get_balance` | 185 | | `GET /payments/{imp_uid}` | `Payments` | `get` | 186 | | `GET /payments/find/{merchant_uid}/{payment_status}` | `Payments` | `get_find` | 187 | | `GET /payments/findAll/{merchant_uid}/{payment_status}` | `Payments` | `get_findall` | 188 | | `POST /subscribe/payments/onetime` | `Subscribe` | `post_payments_onetime` | 189 | | `POST /subscribe/payments/again` | `Subscribe` | `post_payments_again` | 190 | | `DELETE /subscribe/customers/{customer_uid}` | `Subscribe` | `delete_customers` | 191 | 192 | ### 대응되는 Method가 추가되어 있는 API 호출 193 | 194 | ```python 195 | from iamporter.api import Payments 196 | 197 | api_instance = Payments(auth) 198 | response = api_instance.get("your_imp_uid") 199 | ``` 200 | 201 | ### 대응되는 Method가 없는 API 호출 202 | 203 | ```python 204 | from iamporter.api import Escrows 205 | 206 | api_instance = Escrows(auth) 207 | response = api_instance._post('/logis/{imp_uid}'.format(imp_uid="your_imp_uid"), sender="", receiver="", logis="") 208 | ``` 209 | 210 | ### 응답 처리 211 | 212 | 모든 API Level의 응답은 `IamportResponse` 인스턴스로 반환됩니다. 213 | 214 | ```python 215 | response.status # HTTP Status Code 216 | response.code # API 응답 code 필드 217 | response.message # API 응답 message 필드 218 | response.data # API 응답 response 필드 219 | response.is_succeed # API 결과 OK 여부 220 | response.raw # API 응답 원문 (dict) 221 | ``` 222 | 223 | 224 | ## Contribution 225 | 226 | 본 프로젝트는 어떠한 형태의 기여라도 환영합니다. Issue나 PR를 올려주시면 빠른 시간 안에 확인하겠습니다. 227 | 기타 문의는 kde713@gmail.com 으로 부탁드립니다. 228 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm dist/* 4 | python setup.py bdist_wheel 5 | 6 | if [ $# -ge 1 ] && [ $1 == "test" ]; then 7 | twine upload --repository-url https://test.pypi.org/legacy/ dist/* 8 | else 9 | twine upload dist/* 10 | fi 11 | 12 | rm -rf build -------------------------------------------------------------------------------- /iamporter/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import IamportResponse, IamportAuth 2 | from . import api, consts, errors 3 | from .client import Iamporter 4 | 5 | __version__ = "0.2.4" 6 | 7 | __all__ = ['__version__', 8 | 'IamportResponse', 'IamportAuth', 9 | 'api', 'consts', 'errors', 10 | 'Iamporter', ] 11 | -------------------------------------------------------------------------------- /iamporter/api.py: -------------------------------------------------------------------------------- 1 | from .base import IamportResponse, BaseApi 2 | 3 | 4 | class Certifications(BaseApi): 5 | NAMESPACE = "certifications" 6 | 7 | 8 | class Cards(BaseApi): 9 | NAMESPACE = "cards" 10 | 11 | 12 | class Banks(BaseApi): 13 | NAMESPACE = "banks" 14 | 15 | 16 | class Escrows(BaseApi): 17 | NAMESPACE = "escrows" 18 | 19 | 20 | class Naver(BaseApi): 21 | NAMESPACE = "naver" 22 | 23 | 24 | class Payco(BaseApi): 25 | NAMESPACE = "payco" 26 | 27 | 28 | class Payments(BaseApi): 29 | NAMESPACE = "payments" 30 | 31 | def get_balance(self, imp_uid): 32 | """결제수단별 금액 상세 정보 확인 33 | 아임포트 고유번호로 결제수단별 금액 상세정보를 확인합니다.(현재, PAYCO결제수단에 한해 제공되고 있습니다.) 34 | 35 | Args: 36 | imp_uid (str): 아임포트 고유번호 37 | 38 | Returns: 39 | IamportResponse 40 | """ 41 | return self._get('/{imp_uid}/balance'.format(imp_uid=imp_uid)) 42 | 43 | def get(self, imp_uid): 44 | """아임포트 고유번호로 결제내역을 확인합니다 45 | 46 | Args: 47 | imp_uid (str): 아임포트 고유번호 48 | 49 | Returns: 50 | IamportResponse 51 | """ 52 | return self._get('/{imp_uid}'.format(imp_uid=imp_uid)) 53 | 54 | def get_find(self, merchant_uid, payment_status=None, sorting=None): 55 | """가맹점지정 고유번호로 결제내역을 확인합니다 56 | 동일한 merchant_uid가 여러 건 존재하는 경우, 정렬 기준에 따라 가장 첫 번째 해당되는 건을 반환합니다. 57 | (모든 내역에 대한 조회가 필요하시면 get_findall을 사용해주세요.) 58 | 59 | Args: 60 | merchant_uid (str): 결제요청 시 가맹점에서 요청한 merchant_uid 61 | payment_status (str): 특정 status상태의 값만 필터링하고 싶은 경우에 사용. 지정하지 않으면 모든 상태를 대상으로 조회합니다. 62 | sorting (str): 정렬기준. 기본값은 -started. 63 | 64 | Returns: 65 | IamportResponse 66 | """ 67 | payment_status = "/" + payment_status if payment_status else "" 68 | params = self._build_params(sorting=sorting) 69 | return self._get('/find/{merchant_uid}{payment_status}'.format(merchant_uid=merchant_uid, 70 | payment_status=payment_status), 71 | **params) 72 | 73 | def get_findall(self, merchant_uid, payment_status=None, page=None, sorting=None): 74 | """가맹점지정 고유번호로 결제내역을 확인합니다 75 | 76 | Args: 77 | merchant_uid (str): 결제요청 시 가맹점에서 요청한 merchant_uid 78 | payment_status (str): 특정 status상태의 값만 필터링하고 싶은 경우에 사용. 지정하지 않으면 모든 상태를 대상으로 조회합니다. 79 | page (int): 1부터 시작. 기본값 1 80 | sorting (str): 정렬기준. 기본값은 -started. 81 | 82 | Returns: 83 | IamportResponse 84 | """ 85 | payment_status = "/" + payment_status if payment_status else "" 86 | params = self._build_params(page=page, sorting=sorting) 87 | return self._get('/findAll/{merchant_uid}{payment_status}'.format(merchant_uid=merchant_uid, 88 | payment_status=payment_status), 89 | **params) 90 | 91 | def get_status(self, payment_status, page=None, limit=None, search_from=None, search_to=None, sorting=None): 92 | """미결제/결제완료/결제취소/결제실패 상태 별로 검색(20건씩 최신순 페이징) 93 | 미결제/결제완료/결제취소/결제실패 상태 별로 검색할 수 있습니다.(20건씩 최신순 페이징) 94 | 검색기간은 최대 90일까지이며 to파라메터의 기본값은 현재 unix timestamp이고 from파라메터의 기본값은 to파라메터 기준으로 90일 전입니다. 때문에, from/to 파라메터가 없이 호출되면 현재 시점 기준으로 최근 90일 구간에 대한 데이터를 검색하게 됩니다. 95 | from, to 파라메터를 지정하여 90일 단위로 과거 데이터 조회는 가능합니다. 96 | 97 | Args: 98 | payment_status (str) 99 | page (int): 1부터 시작. 기본값 1 100 | limit (int): 한 번에 조회할 결제건수.(최대 100건, 기본값 20건) 101 | search_from (int): 시간별 검색 시작 시각(>=) UNIX TIMESTAMP. 결제건의 최종 status에 따라 다른 검색기준이 적용됩니다. 기본값은 to 파라메터 기준으로 90일 전 unix timestamp. 102 | search_to (int): 시간별 검색 종료 시각(<=) UNIX TIMESTAMP. 결제건의 최종 status에 따라 다른 검색기준이 적용됩니다. 기본값은 현재 unix timestamp. 103 | sorting (str): 정렬기준. 기본값은 -started 104 | 105 | Returns: 106 | IamportResponse 107 | """ 108 | params = self._build_params( 109 | **{'page': page, 'limit': limit, 'from': search_from, 'to': search_to, 'sorting': sorting}) 110 | return self._get('/status/{payment_status}'.format(payment_status=payment_status), **params) 111 | 112 | def post_cancel(self, imp_uid=None, merchant_uid=None, amount=None, tax_free=None, checksum=None, reason=None, 113 | refund_holder=None, refund_bank=None, refund_account=None): 114 | """승인된 결제를 취소합니다. 115 | 신용카드/실시간계좌이체/휴대폰소액결제의 경우 즉시 취소처리가 이뤄지게 되며, 가상계좌의 경우는 환불받으실 계좌정보를 같이 전달해주시면 환불정보가 PG사에 등록되어 익영업일에 처리됩니다.(가상계좌 환불관련 특약계약 필요) 116 | 117 | Args: 118 | imp_uid (str): 취소할 거래의 아임포트 고유번호 119 | merchant_uid (str): 가맹점에서 전달한 거래 고유번호. imp_uid, merchant_uid 중 하나는 필수이어야 합니다. 두 값이 모두 넘어오면 imp_uid를 우선 적용합니다. 120 | amount (float): (부분)취소요청금액(누락이면 전액취소) 121 | tax_free (float): (부분)취소요청금액 중 면세금액(누락되면 0원처리) 122 | checksum (float): 취소 트랜잭션 수행 전, 현재시점의 취소 가능한 잔액. 누락 시 검증 프로세스를 생략합니다. 123 | reason (str): 취소 사유 124 | refund_holder (str): 환불계좌 예금주(가상계좌취소시 필수) 125 | refund_bank (str): 환불계좌 은행코드(하단 은행코드표 참조, 가상계좌취소시 필수) 126 | refund_account (str): 환불계좌 계좌번호(가상계좌취소시 필수) 127 | 128 | Returns: 129 | IamportResponse 130 | """ 131 | params = self._build_params(**{ 132 | 'imp_uid': imp_uid, 133 | 'merchant_uid': merchant_uid, 134 | 'amount': amount, 135 | 'tax_free': tax_free, 136 | 'checksum': checksum, 137 | 'reason': reason, 138 | 'refund_holder': refund_holder, 139 | 'refund_bank': refund_bank, 140 | 'refund_account': refund_account, 141 | }) 142 | return self._post('/cancel', **params) 143 | 144 | 145 | class Receipts(BaseApi): 146 | NAMESPACE = "receipts" 147 | 148 | 149 | class Subscribe(BaseApi): 150 | NAMESPACE = "subscribe" 151 | 152 | def get_customers(self, customer_uid): 153 | """구매자의 빌링키 정보 조회 154 | 155 | Args: 156 | customer_uid (str): 구매자 고유 번호 157 | 158 | Returns: 159 | IamportResponse 160 | """ 161 | return self._get('/customers/{customer_uid}'.format(customer_uid=customer_uid)) 162 | 163 | def post_customers(self, customer_uid, card_number, expiry, birth, pwd_2digit=None, pg=None, 164 | customer_name=None, customer_tel=None, customer_email=None, customer_addr=None, 165 | customer_postcode=None): 166 | """구매자에 대해 빌링키 발급 및 저장 167 | 168 | Args: 169 | customer_uid (str): 구매자 고유 번호 170 | card_number (str): 카드번호 (dddd-dddd-dddd-dddd) 171 | expiry (str): 카드 유효기간 (YYYY-MM) 172 | birth (str): 생년월일6자리 (법인카드의 경우 사업자등록번호10자리) 173 | pwd_2digit (str): 카드비밀번호 앞 2자리 (법인카드의 경우 생략가능) 174 | pg (str): API 방식 비인증 PG설정이 2개 이상인 경우, 결제가 진행되길 원하는 PG사를 지정하실 수 있습니다. 175 | customer_name (str): 고객(카드소지자) 관리용 성함 176 | customer_tel (str): 고객(카드소지자) 전화번호 177 | customer_email (str): 고객(카드소지자) Email 178 | customer_addr (str): 고객(카드소지자) 주소 179 | customer_postcode (str): 고객(카드소지자) 우편번호 180 | 181 | Returns: 182 | IamportResponse 183 | """ 184 | params = self._build_params(**{ 185 | 'card_number': card_number, 186 | 'expiry': expiry, 187 | 'birth': birth, 188 | 'pwd_2digit': pwd_2digit, 189 | 'pg': pg, 190 | 'customer_name': customer_name, 191 | 'customer_tel': customer_tel, 192 | 'customer_email': customer_email, 193 | 'customer_addr': customer_addr, 194 | 'customer_postcode': customer_postcode, 195 | }) 196 | return self._post('/customers/{customer_uid}'.format(customer_uid=customer_uid), **params) 197 | 198 | def delete_customers(self, customer_uid): 199 | """구매자의 빌링키 정보 삭제(DB에서 빌링키를 삭제[delete] 합니다) 200 | 201 | Args: 202 | customer_uid (str): 구매자 고유 번호 203 | 204 | Returns: 205 | IamportResponse 206 | """ 207 | return self._delete('/customers/{customer_uid}'.format(customer_uid=customer_uid)) 208 | 209 | def get_customers_payments(self, customer_uid, page=None): 210 | """구매자의 빌링키로 결제된 결제목록 조회 211 | 212 | Args: 213 | customer_uid (str): 구매자 고유번 214 | page (int): 페이징 페이지. 1부터 시작 215 | 216 | Returns: 217 | IamportResponse 218 | """ 219 | params = self._build_params(page=page) 220 | return self._get('/customers/{customer_uid}/payments'.format(customer_uid=customer_uid), **params) 221 | 222 | def post_payments_onetime(self, merchant_uid, amount, card_number, expiry, birth=None, pwd_2digit=None, 223 | vat=None, customer_uid=None, pg=None, name=None, 224 | buyer_name=None, buyer_email=None, buyer_tel=None, buyer_addr=None, buyer_postcode=None, 225 | card_quota=None, custom_data=None): 226 | """구매자로부터 별도의 인증과정을 거치지 않고, 카드정보만으로 결제를 진행하는 API 227 | customer_uid를 전달해주시면 결제 후 다음 번 결제를 위해 성공된 결제에 사용된 빌링키를 저장해두게되고, customer_uid가 없는 경우 저장되지 않습니다. 228 | 동일한 merchant_uid는 재사용이 불가능하며 고유한 값을 전달해주셔야 합니다. 229 | 빌링키 저장 시, buyer_email, buyer_name 등의 정보는 customer 부가정보인 customer_email, customer_name 등으로 함께 저장됩니다. 230 | .post_customers 참조 231 | 232 | Args: 233 | merchant_uid (str): 가맹점 거래 고유번호 234 | amount (float): 결제금액 235 | card_number (str): 카드번호 (dddd-dddd-dddd-dddd) 236 | expiry (str): 카드 유효기간 (YYYY-MM) 237 | birth (str): 생년월일6자리 (법인카드의 경우 사업자등록번호10자리) 238 | pwd_2digit (str): 카드비밀번호 앞 2자리 (법인카드의 경우 생략가능) 239 | vat (float): 결제금액 중 부가세 금액 (파라메터가 누락되면 10%로 자동 계산됨) 240 | customer_uid (str): string 타입의 고객 고유번호. 241 | pg (str): API 방식 비인증 PG설정이 2개 이상인 경우, 결제가 진행되길 원하는 PG사를 지정하실 수 있습니다. 242 | name (str): 주문명 243 | buyer_name (str): 주문자명 244 | buyer_email (str): 주문자 E-mail주소 245 | buyer_tel (str): 주문자 전화번호 246 | buyer_addr (str): 주문자 주소 247 | buyer_postcode (str): 주문자 우편번 248 | card_quota (int): 카드할부개월수. 2 이상의 integer 할부개월수 적용 (결제금액 50,000원 이상 한정) 249 | custom_data (str): 거래정보와 함께 저장할 추가 정보 250 | 251 | Returns: 252 | IamportResponse 253 | """ 254 | params = self._build_params(**{ 255 | 'merchant_uid': merchant_uid, 256 | 'amount': amount, 257 | 'card_number': card_number, 258 | 'expiry': expiry, 259 | 'birth': birth, 260 | 'pwd_2digit': pwd_2digit, 261 | 'vat': vat, 262 | 'customer_uid': customer_uid, 263 | 'pg': pg, 264 | 'name': name, 265 | 'buyer_name': buyer_name, 266 | 'buyer_email': buyer_email, 267 | 'buyer_tel': buyer_tel, 268 | 'buyer_addr': buyer_addr, 269 | 'buyer_postcode': buyer_postcode, 270 | 'card_quota': card_quota, 271 | 'custom_data': custom_data, 272 | }) 273 | return self._post('/payments/onetime', **params) 274 | 275 | def post_payments_again(self, customer_uid, merchant_uid, amount, name, vat=None, 276 | buyer_name=None, buyer_email=None, buyer_tel=None, buyer_addr=None, buyer_postcode=None, 277 | card_quota=None, custom_data=None): 278 | """저장된 빌링키로 재결제를 하는 경우 사용됩니다. 279 | .post_payments_onetime 또는 Subscribe.post_customers 로 등록된 빌링키가 있을 때 매칭되는 customer_uid로 재결제를 진행할 수 있습니다. 280 | 281 | Args: 282 | customer_uid (str): string 타입의 고객 고유번호 283 | merchant_uid (str): 가맹점 거래 고유번호 284 | amount (float): 결제금액 285 | name (str): 주문명 286 | vat (float): 결제금액 중 부가세 금액(파라메터가 누락되면 10%로 자동 계산됨) 287 | buyer_name (str): 주문자명 288 | buyer_email (str): 주문자 E-mail주소 289 | buyer_tel (str): 주문자 전화번호 290 | buyer_addr (str): 주문자 주소 291 | buyer_postcode (str): 주문자 우편번 292 | card_quota (int): 카드할부개월수. 2 이상의 integer 할부개월수 적용 (결제금액 50,000원 이상 한정) 293 | custom_data (str): 거래정보와 함께 저장할 추가 정보 294 | 295 | Returns: 296 | IamportResponse 297 | """ 298 | params = self._build_params(**{ 299 | 'customer_uid': customer_uid, 300 | 'merchant_uid': merchant_uid, 301 | 'amount': amount, 302 | 'name': name, 303 | 'vat': vat, 304 | 'buyer_name': buyer_name, 305 | 'buyer_email': buyer_email, 306 | 'buyer_tel': buyer_tel, 307 | 'buyer_addr': buyer_addr, 308 | 'buyer_postcode': buyer_postcode, 309 | 'card_quota': card_quota, 310 | 'custom_data': custom_data, 311 | }) 312 | return self._post('/payments/again', **params) 313 | 314 | 315 | class VBanks(BaseApi): 316 | NAMESPACE = "vbanks" 317 | -------------------------------------------------------------------------------- /iamporter/base.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | import requests 4 | from requests.auth import AuthBase 5 | 6 | from .consts import IAMPORT_API_URL 7 | from .errors import ImpUnAuthorized 8 | 9 | 10 | def build_url(base_url: str, path: str = "/"): 11 | extracted_base_url = urllib.parse.urlparse(base_url) 12 | if path[0] != "/": 13 | path = "/" + path 14 | return urllib.parse.urlunparse(( 15 | extracted_base_url[0], extracted_base_url[1], path, '', '', '' 16 | )) 17 | 18 | 19 | class IamportResponse: 20 | """아임포트 API 응답 객체 21 | 22 | Attributes: 23 | status (int): API 응답 HTTP 상태 코드 24 | code (int): API 응답코드 25 | message (str): API 응답메세지 26 | data (dict): API 응답 response 데이터 27 | """ 28 | 29 | def __init__(self, requests_response): 30 | """ 31 | Args: 32 | requests_response (requests.Response) 33 | """ 34 | self.status = requests_response.status_code 35 | 36 | body = requests_response.json() 37 | self.code = body.get('code') 38 | self.message = body.get('message') 39 | self.data = body.get('response', {}) 40 | 41 | @property 42 | def is_succeed(self): 43 | """API 결과가 성공적인지 확인""" 44 | return self.status == 200 and self.code == 0 45 | 46 | @property 47 | def raw(self): 48 | """원본 API 응답""" 49 | return { 50 | 'code': self.code, 51 | 'message': self.message, 52 | 'response': self.data, 53 | } 54 | 55 | 56 | class IamportAuth(AuthBase): 57 | """아임포트 인증 객체 58 | 59 | Attributes: 60 | token (str): 발급받은 액세스 토큰 61 | """ 62 | 63 | def __init__(self, imp_key, imp_secret, session=None, imp_url=IAMPORT_API_URL): 64 | """ 65 | Args: 66 | imp_key (str): 아임포트 API 키 67 | imp_secret (str): 아임포트 API 시크릿 68 | session (requests.Session): API 요청에 사용할 requests Session 인스턴스 69 | imp_url (str): 아임포트 API URL 70 | """ 71 | 72 | self.token = None 73 | 74 | api_endpoint = build_url(imp_url, '/users/getToken') 75 | api_payload = {'imp_key': imp_key, 'imp_secret': imp_secret} 76 | 77 | auth_response = IamportResponse( 78 | session.post(api_endpoint, data=api_payload) if isinstance(session, requests.Session) 79 | else requests.post(api_endpoint, data=api_payload) 80 | ) 81 | if auth_response.is_succeed: 82 | self.token = auth_response.data.get('access_token', None) 83 | 84 | if session: 85 | session.close() 86 | 87 | if self.token is None: 88 | raise ImpUnAuthorized(auth_response.message) 89 | 90 | def __call__(self, r): 91 | r.headers['Authorization'] = self.token 92 | return r 93 | 94 | 95 | class BaseApi: 96 | """모든 API 객체의 공통 요소 상속용 추상 객체 97 | 98 | Attributes: 99 | requests_session (requests.Session): API 호출에 사용될 requests Session 인스턴스 100 | """ 101 | NAMESPACE = "" 102 | 103 | def __init__(self, auth, session=None, imp_url=IAMPORT_API_URL): 104 | """ 105 | Args: 106 | auth (IamportAuth): 아임포트 API 인증 인스턴스 107 | session (requests.Session): API 요청에 사용할 requests Session 인스턴스 108 | imp_url (str): 아임포트 API URL 109 | """ 110 | self.iamport_auth = auth 111 | self.requests_session = session 112 | self.imp_url = imp_url 113 | 114 | def _build_url(self, endpoint): 115 | return build_url(self.imp_url, self.NAMESPACE + endpoint) 116 | 117 | def _build_params(self, **kwargs): 118 | """None이 아닌 value를 가진 key만 포함된 dict를 반환합니다. 119 | 120 | Args: 121 | **kwargs 122 | 123 | Returns: 124 | dict 125 | """ 126 | params = {} 127 | for key, value in kwargs.items(): 128 | if value is not None: 129 | params[key] = value 130 | return params 131 | 132 | def _get(self, endpoint, **kwargs): 133 | """GET 요청을 보내고 그 결과를 IamportResponse 객체로 리턴합니다. 134 | 135 | Args: 136 | endpoint (str): API Endpoint 137 | **kwargs 138 | 139 | Returns: 140 | IamportResponse 141 | """ 142 | if isinstance(self.requests_session, requests.Session): 143 | return IamportResponse( 144 | self.requests_session.get(self._build_url(endpoint), auth=self.iamport_auth, params=kwargs)) 145 | 146 | return IamportResponse(requests.get(self._build_url(endpoint), auth=self.iamport_auth, params=kwargs)) 147 | 148 | def _post(self, endpoint, **kwargs): 149 | """POST 요청을 보내고 그 결과를 IamportResponse 객체로 리턴합니다. 150 | 151 | Args: 152 | endpoint (str): API Endpoint 153 | **kwargs 154 | 155 | Returns: 156 | IamportResponse 157 | """ 158 | if isinstance(self.requests_session, requests.Session): 159 | return IamportResponse( 160 | self.requests_session.post(self._build_url(endpoint), auth=self.iamport_auth, data=kwargs)) 161 | 162 | return IamportResponse(requests.post(self._build_url(endpoint), auth=self.iamport_auth, data=kwargs)) 163 | 164 | def _delete(self, endpoint): 165 | """DELETE 요청을 보내고 그 결과를 IamportResponse 객체로 리턴합니다. 166 | 167 | Args: 168 | endpoint (str): API Endpoint 169 | 170 | Returns: 171 | IamportResponse 172 | """ 173 | if isinstance(self.requests_session, requests.Session): 174 | return IamportResponse( 175 | self.requests_session.delete(self._build_url(endpoint), auth=self.iamport_auth)) 176 | 177 | return IamportResponse(requests.delete(self._build_url(endpoint), auth=self.iamport_auth)) 178 | -------------------------------------------------------------------------------- /iamporter/client.py: -------------------------------------------------------------------------------- 1 | from requests import Session 2 | from requests.adapters import HTTPAdapter 3 | 4 | from .base import IamportAuth, IamportResponse 5 | from .errors import ImpUnAuthorized, ImpApiError 6 | from .api import Payments, Subscribe 7 | from .consts import IAMPORT_API_URL 8 | 9 | 10 | class Iamporter: 11 | """Iamport Client 객체 12 | api-level의 api Class를 보다 사용하기 편하게 wrapping한 객체 13 | 14 | Attributes: 15 | imp_auth (IamportAuth): 아임포트 인증 인스턴스 16 | imp_url (str): Iamport REST API Host 17 | requests_session (Session): 아임포트 API 호출에 사용될 세션 객체 18 | """ 19 | 20 | def __init__(self, imp_key=None, imp_secret=None, imp_auth=None, imp_url=IAMPORT_API_URL): 21 | """ 22 | imp_key와 imp_secret을 전달하거나 IamportAuth 인스턴스를 직접 imp_auth로 넘겨 초기화할 수 있습니다. 23 | 24 | Args: 25 | imp_key (str): Iamport REST API Key 26 | imp_secret (str): Iamport REST API Secret 27 | imp_auth (IamportAuth): IamportAuth 인증 인스턴스 28 | imp_url (str): Iamport REST API Host. 기본값은 https://api.iamport.kr/ 29 | """ 30 | if isinstance(imp_auth, IamportAuth): 31 | self.imp_auth = imp_auth 32 | elif imp_key and imp_secret: 33 | self.imp_auth = IamportAuth(imp_key, imp_secret, imp_url=imp_url) 34 | else: 35 | raise ImpUnAuthorized("인증정보가 전달되지 않았습니다.") 36 | 37 | self.imp_url = imp_url 38 | 39 | self.requests_session = Session() 40 | requests_adapter = HTTPAdapter(max_retries=3) 41 | self.requests_session.mount('https://', requests_adapter) 42 | 43 | def __del__(self): 44 | if getattr(self, 'requests_session', None): 45 | self.requests_session.close() 46 | 47 | @property 48 | def _api_kwargs(self): 49 | return {'auth': self.imp_auth, 'session': self.requests_session, 'imp_url': self.imp_url} 50 | 51 | def _process_response(self, response): 52 | """ 53 | Args: 54 | response (IamportResponse) 55 | 56 | Returns: 57 | dict 58 | """ 59 | if response.status == 401: 60 | raise ImpUnAuthorized(response.message) 61 | if not response.is_succeed: 62 | raise ImpApiError(response) 63 | return response.data 64 | 65 | def find_payment(self, imp_uid=None, merchant_uid=None): 66 | """아임포트 고유번호 또는 가맹점지정 고유번호로 결제내역을 확인합니다 67 | 68 | Args: 69 | imp_uid (str): 아임포트 고유번호 70 | merchant_uid (str): 결제요청 시 가맹점에서 요청한 merchant_uid. imp_uid와 merchant_uid 중 하나는 필수어야합니다. 두 값이 모두 넘어오면 imp_uid를 우선 적용합니다. 71 | 72 | Returns: 73 | dict 74 | """ 75 | api_instance = Payments(**self._api_kwargs) 76 | if imp_uid: 77 | response = api_instance.get(imp_uid) 78 | elif merchant_uid: 79 | response = api_instance.get_find(merchant_uid) 80 | else: 81 | raise KeyError('imp_uid와 merchant_uid 중 하나를 반드시 지정해야합니다.') 82 | 83 | return self._process_response(response) 84 | 85 | def cancel_payment(self, imp_uid=None, merchant_uid=None, amount=None, tax_free=None, reason=None): 86 | """승인된 결제를 취소합니다. 87 | 88 | Args: 89 | imp_uid (str): 아임포트 고유 번호 90 | merchant_uid (str): 가맹점지정 고유 번호. imp_uid와 merchant_uid 중 하나는 필수이어야합니다. 두 값이 모두 넘어오면 imp_uid를 우선 적용합니다. 91 | amount (float): 취소 요청 금액. 누락 시 전액을 취소합니다. 92 | tax_free (float): 취소 요청 금액 중 면세 금액. 누락 시 0원으로 간주합니다. 93 | reason (str): 취소 사유 94 | 95 | Returns: 96 | dict 97 | """ 98 | if not (imp_uid or merchant_uid): 99 | raise KeyError('imp_uid와 merchant_uid 중 하나를 반드시 지정해야합니다.') 100 | 101 | api_instance = Payments(**self._api_kwargs) 102 | response = api_instance.post_cancel(imp_uid=imp_uid, merchant_uid=merchant_uid, 103 | amount=amount, tax_free=tax_free, 104 | reason=reason, ) 105 | 106 | return self._process_response(response) 107 | 108 | def create_billkey(self, customer_uid=None, card_number=None, expiry=None, birth=None, pwd_2digit=None, pg=None, 109 | customer_info=None): 110 | """정기결제 등에 사용하는 비인증결제를 위한 빌링키를 발급합니다. 111 | 112 | Args: 113 | customer_uid (str): 구매자 고유 번호 114 | card_number (str): 카드번호 (dddd-dddd-dddd-dddd) 115 | expiry (str): 카드 유효기간 (YYYY-MM) 116 | birth (str): 생년월일6자리 (법인카드의 경우 사업자등록번호10자리) 117 | pwd_2digit (str): 카드비밀번호 앞 2자리 (법인카드의 경우 생략가능) 118 | pg (str): API 방식 비인증 PG설정이 2개 이상인 경우, 결제가 진행되길 원하는 PG사를 지정하실 수 있습니다. 119 | customer_info (dict): 고객(카드소지자) 정보 (name, tel, email, addr, postcode) 120 | 121 | Returns: 122 | dict 123 | """ 124 | if not (customer_uid and card_number and expiry and birth): 125 | raise KeyError('customer_uid, card_number, expiry, birth는 필수값입니다.') 126 | if not customer_info: 127 | customer_info = {} 128 | 129 | api_instance = Subscribe(**self._api_kwargs) 130 | response = api_instance.post_customers(customer_uid, card_number, expiry, birth, pwd_2digit=pwd_2digit, pg=pg, 131 | customer_name=customer_info.get('name'), 132 | customer_tel=customer_info.get('tel'), 133 | customer_email=customer_info.get('email'), 134 | customer_addr=customer_info.get('addr'), 135 | customer_postcode=customer_info.get('postcode')) 136 | 137 | return self._process_response(response) 138 | 139 | def find_billkey(self, customer_uid=None): 140 | """빌링키 정보를 조회합니다 141 | 142 | Args: 143 | customer_uid (str): 구매자 고유번호 144 | 145 | Returns: 146 | dict 147 | """ 148 | if not customer_uid: 149 | raise KeyError('customer_uid는 필수값입니다.') 150 | 151 | api_instance = Subscribe(**self._api_kwargs) 152 | response = api_instance.get_customers(customer_uid) 153 | 154 | return self._process_response(response) 155 | 156 | def delete_billkey(self, customer_uid=None): 157 | """빌링키를 삭제합니다 158 | 159 | Args: 160 | customer_uid (str): 구매자 고유번호 161 | 162 | Returns: 163 | dict 164 | """ 165 | if not customer_uid: 166 | raise KeyError('customer_uid는 필수값입니다.') 167 | 168 | api_instance = Subscribe(**self._api_kwargs) 169 | response = api_instance.delete_customers(customer_uid) 170 | 171 | return self._process_response(response) 172 | 173 | def create_payment(self, merchant_uid=None, customer_uid=None, name=None, amount=None, vat=None, 174 | card_number=None, expiry=None, birth=None, pwd_2digit=None, pg=None, 175 | buyer_info=None, card_quota=None, custom_data=None): 176 | """카드정보 또는 빌링키로 결제를 요청합니다 177 | 카드정보를 지정하여 일회성 키인 결제를 요청할 수 있으며, 빌링키(customer_uid)를 지정해 재결제를 요청할 수 있습니다. 178 | 카드정보와 빌링키가 모두 지정되면 일회성 결제 수행 후 해당 카드정보를 바탕으로 빌링키를 저장합니다. 179 | 180 | Args: 181 | merchant_uid (str): 가맹점 거래 고유번호 182 | customer_uid (str): string 타입의 고객 고유번호 183 | name (str): 주문명 184 | amount (float): 결제금액 185 | vat (float): 결제금액 중 부가세 금액 (파라메터가 누락되면 10%로 자동 계산됨) 186 | card_number (str): 카드번호 (dddd-dddd-dddd-dddd) 187 | expiry (str): 카드 유효기간 (YYYY-MM) 188 | birth (str): 생년월일6자리 (법인카드의 경우 사업자등록번호10자리) 189 | pwd_2digit (str): 카드비밀번호 앞 2자리 (법인카드의 경우 생략가능) 190 | pg (str): API 방식 비인증 PG설정이 2개 이상인 경우, 결제가 진행되길 원하는 PG사를 지정하실 수 있습니다. 191 | buyer_info (dict): 구매자 정보 (name, tel, email, addr, postcode) 192 | card_quota (int): 카드할부개월수. 2 이상의 integer 할부개월수 적용 (결제금액 50,000원 이상 한정) 193 | custom_data (str): 거래정보와 함께 저장할 추가 정보 194 | 195 | Returns: 196 | dict 197 | """ 198 | if not (merchant_uid and name and amount): 199 | raise KeyError('merchant_uid, name, amount는 필수값입니다.') 200 | if not ((card_number and expiry) or customer_uid): 201 | raise KeyError('카드 정보 또는 customer_uid 중 하나 이상은 반드시 포함되어야합니다.') 202 | if not buyer_info: 203 | buyer_info = {} 204 | 205 | api_instance = Subscribe(**self._api_kwargs) 206 | if card_number and expiry: 207 | response = api_instance.post_payments_onetime(merchant_uid, amount, card_number, expiry, 208 | birth=birth, pwd_2digit=pwd_2digit, 209 | vat=vat, customer_uid=customer_uid, 210 | pg=pg, name=name, 211 | buyer_name=buyer_info.get('name'), 212 | buyer_email=buyer_info.get('email'), 213 | buyer_tel=buyer_info.get('tel'), 214 | buyer_addr=buyer_info.get('addr'), 215 | buyer_postcode=buyer_info.get('postcode'), 216 | card_quota=card_quota, custom_data=custom_data) 217 | else: 218 | response = api_instance.post_payments_again(customer_uid, merchant_uid, amount, name, vat=vat, 219 | buyer_name=buyer_info.get('name'), 220 | buyer_email=buyer_info.get('email'), 221 | buyer_tel=buyer_info.get('tel'), 222 | buyer_addr=buyer_info.get('addr'), 223 | buyer_postcode=buyer_info.get('postcode'), 224 | card_quota=card_quota, 225 | custom_data=custom_data) 226 | 227 | return self._process_response(response) 228 | -------------------------------------------------------------------------------- /iamporter/consts.py: -------------------------------------------------------------------------------- 1 | IAMPORT_API_URL = "https://api.iamport.kr/" 2 | 3 | IMP_STATUS_ALL = "all" 4 | IMP_STATUS_READY = "ready" 5 | IMP_STATUS_PAID = "paid" 6 | IMP_STATUS_CANCELED = "cancelled" 7 | IMP_STATUS_FAILED = "failed" 8 | 9 | IMP_SORTING_STARTED_DESC = "-started" 10 | IMP_SORTING_STARTED_ASC = "started" 11 | IMP_SORTING_PAID_DESC = "-paid" 12 | IMP_SORTING_PAID_ASC = "paid" 13 | IMP_SORTING_UPDATED_DESC = "-updated" 14 | IMP_SORTING_UPDATED_ASC = "updated" 15 | 16 | IMP_PG_INICIS_WEBSTD = "html5_inicis" # INICIS 웹표준 17 | IMP_PG_INICIS_ACTIVEX = "inicis" # INICIS Active-X 18 | IMP_PG_NHNKCP = "kcp" # NHNKCP 19 | IMP_PG_NHNKCP_SUBSCRIBE = "kcp_billing" # NHN KCP 정기결제 20 | IMP_PG_LGUPLUS = "uplus" # LG U+ 21 | IMP_PG_NICEPAY = "nice" # 나이스페이 22 | IMP_PG_JTNET = "jtnet" # JTNet 23 | IMP_PG_KAKAOPAY = "kakao" # 카카오페이 24 | IMP_PG_DANAL_PHONE = "danal" # 다날휴대폰소액결제 25 | IMP_PG_DANAL_GENERAL = "danal_tpay" # 다날일반결제 26 | IMP_PG_MOBILIANS = "mobilians" # 모빌리언휴대폰소액결제 27 | IMP_PG_SYRUPPAY = "syrup" # 시럽페이 28 | IMP_PG_PAYCO = "payco" # 페이코 29 | IMP_PG_PAYPAL = "paypal" # 페이팔 30 | IMP_PG_EXIMBAY = "eximbay" # 엑심베이 31 | IMP_PG_NAVERPAY = "naverco" # 네이버페이 32 | -------------------------------------------------------------------------------- /iamporter/errors.py: -------------------------------------------------------------------------------- 1 | class ImpApiError(Exception): 2 | def __init__(self, response): 3 | self.response = response 4 | 5 | def __str__(self): 6 | return "아임포트 API 오류 (status={status}, message={message})".format( 7 | status=self.response.status, message=self.response.message 8 | ) 9 | 10 | 11 | class ImpUnAuthorized(Exception): 12 | def __init__(self, message): 13 | self.message = message 14 | 15 | def __str__(self): 16 | return "아임포트 인증 실패 (message={message})".format(message=self.message) 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ast 4 | import re 5 | 6 | from setuptools import find_packages, setup 7 | 8 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 9 | 10 | with open('iamporter/__init__.py', 'rb') as f: 11 | version = str(ast.literal_eval(_version_re.search( 12 | f.read().decode('utf-8')).group(1))) 13 | 14 | setup( 15 | name='iamporter', 16 | version=version, 17 | 18 | description="An I'mport REST API Client for Human", 19 | long_description='아임포트에서 제공하는 REST API를 쉽게 활용하기 위해 작성된 Python 클라이언트입니다', 20 | 21 | url='https://github.com/kde713/iamporter-python', 22 | 23 | author='kde713', 24 | author_email='kde713@gmail.com', 25 | 26 | license='MIT', 27 | 28 | classifiers=[ 29 | 'Environment :: Web Environment', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent', 33 | 'Topic :: Software Development :: Libraries :: Python Modules', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.4', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | ], 39 | 40 | keywords=['iamport', 'import', 'payment', 'iamporter'], 41 | 42 | packages=find_packages(exclude=['test', 'tests', 'docs', 'examples']), 43 | 44 | install_requires=[ 45 | 'requests>=2.0.0,<3.0.0', 46 | ], 47 | 48 | python_requires='>=3', 49 | ) 50 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from iamporter import Iamporter, IamportAuth, IamportResponse, errors, consts 4 | from iamporter.base import BaseApi, build_url 5 | 6 | TEST_IMP_KEY = "imp_apikey" 7 | TEST_IMP_SECRET = "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" 8 | 9 | 10 | class TestUrlBuilder(unittest.TestCase): 11 | def test_build_url(self): 12 | self.assertEqual(build_url("https://www.test.com", "not_slashed/path"), 13 | "https://www.test.com/not_slashed/path") 14 | self.assertEqual(build_url("http://www.slashed.host/", "also_slashed/path"), 15 | "http://www.slashed.host/also_slashed/path") 16 | self.assertEqual(build_url("http://www.withport.com:12345", "not_slashed/path"), 17 | "http://www.withport.com:12345/not_slashed/path") 18 | 19 | 20 | class TestIamportResponse(unittest.TestCase): 21 | def setUp(self): 22 | class MockResponse: 23 | def __init__(self, status, response): 24 | self.status_code = status 25 | self.response_body = response 26 | 27 | def json(self): 28 | return self.response_body 29 | 30 | self.VALID_RESPONSE1 = MockResponse(200, {'code': 0, 'message': "가짜 성공 응답", 31 | 'response': {"sample": "sample_data"}}) 32 | self.INVALID_RESPONSE1 = MockResponse(400, {'code': 1, 'message': "가짜 실패 응답", 'response': {}}) 33 | self.INVALID_RESPONSE2 = MockResponse(200, {'code': 2, 'message': "가짜 실패 응답 2", 'response': {}}) 34 | 35 | self.valid_response1 = IamportResponse(self.VALID_RESPONSE1) 36 | self.invalid_response1 = IamportResponse(self.INVALID_RESPONSE1) 37 | self.invalid_response2 = IamportResponse(self.INVALID_RESPONSE2) 38 | 39 | def test_parse(self): 40 | self.assertEqual(self.valid_response1.status, 200) 41 | self.assertEqual(self.valid_response1.code, 0) 42 | self.assertEqual(self.valid_response1.message, "가짜 성공 응답") 43 | self.assertEqual(self.valid_response1.data, {"sample": "sample_data"}) 44 | 45 | def test_is_succeed(self): 46 | self.assertEqual(self.valid_response1.is_succeed, True) 47 | self.assertEqual(self.invalid_response1.is_succeed, False) 48 | self.assertEqual(self.invalid_response2.is_succeed, False) 49 | 50 | def test_raw(self): 51 | self.assertEqual(self.valid_response1.raw, 52 | {'code': 0, 'message': "가짜 성공 응답", 'response': {"sample": "sample_data"}}) 53 | 54 | 55 | class TestIamportAuth(unittest.TestCase): 56 | def test_invalid_auth(self): 57 | self.assertRaises(errors.ImpUnAuthorized, IamportAuth, "invalid_key", "invalid_secret") 58 | 59 | def test_valid_auth(self): 60 | auth = IamportAuth(TEST_IMP_KEY, TEST_IMP_SECRET) 61 | self.assertTrue(auth.token) 62 | 63 | 64 | class TestBaseApi(unittest.TestCase): 65 | def setUp(self): 66 | class SampleBaseApi(BaseApi): 67 | NAMESPACE = "sample" 68 | 69 | self.api_client = SampleBaseApi(None) # BaseApi의 유틸성 메소드만 테스트할 것이기에 인증 과정을 bypass 70 | 71 | def test_build_url(self): 72 | self.assertEqual(self.api_client._build_url("/endpoint"), consts.IAMPORT_API_URL + "sample/endpoint") 73 | 74 | def test_build_params(self): 75 | MOCK_PARAMS = { 76 | 'valid_param1': 123, 77 | 'invalid_param1': None, 78 | 'valid_param2': "", 79 | } 80 | built_params = self.api_client._build_params(**MOCK_PARAMS) 81 | self.assertIn('valid_param1', built_params.keys()) 82 | self.assertNotIn('invalid_param1', built_params.keys()) 83 | self.assertIn('valid_param2', built_params.keys()) 84 | 85 | 86 | class TestIamporter(unittest.TestCase): 87 | def setUp(self): 88 | self.imp_auth = IamportAuth(TEST_IMP_KEY, TEST_IMP_SECRET) 89 | self.client = Iamporter(imp_auth=self.imp_auth) 90 | 91 | def test_init(self): 92 | self.assertRaises(errors.ImpUnAuthorized, Iamporter, imp_key=None) 93 | self.assertRaises(errors.ImpUnAuthorized, Iamporter, imp_key="invalid_key", imp_secret="invalid_secret") 94 | 95 | def test_find_payment(self): 96 | self.assertRaises(KeyError, self.client.find_payment) 97 | self.assertRaises(errors.ImpApiError, self.client.find_payment, imp_uid='test') 98 | self.assertRaises(errors.ImpApiError, self.client.find_payment, merchant_uid='âàáaā') 99 | 100 | def test_cancel_payment(self): 101 | self.assertRaises(errors.ImpApiError, self.client.cancel_payment, imp_uid='nothing', reason='reason') 102 | try: 103 | self.client.cancel_payment(imp_uid='nothing', reason='reason') 104 | except errors.ImpApiError as e: 105 | self.assertEqual(e.response.code, 1) 106 | self.assertEqual(e.response.message, '취소할 결제건이 존재하지 않습니다.') 107 | 108 | try: 109 | self.client.cancel_payment(merchant_uid='any-merchant_uid', reason='any-reason') 110 | except errors.ImpApiError as e: 111 | self.assertEqual(e.response.code, 1) 112 | self.assertEqual(e.response.message, '취소할 결제건이 존재하지 않습니다.') 113 | 114 | self.assertRaises(KeyError, self.client.cancel_payment, merchant_uid=None, reason='some-reason아 ') 115 | 116 | def test_cancel_payment_partital(self): 117 | try: 118 | self.client.cancel_payment(imp_uid='nothing', reason='reason', amount=100) 119 | except errors.ImpApiError as e: 120 | self.assertEqual(e.response.code, 1) 121 | self.assertEqual(e.response.message, '취소할 결제건이 존재하지 않습니다.') 122 | 123 | def test_create_billkey(self): 124 | self.assertRaises(KeyError, self.client.create_billkey, 125 | customer_uid=None, card_number="0000-0000-0000-0000", expiry="2022-06", birth="960714") 126 | 127 | try: 128 | self.client.create_billkey(customer_uid="customer_1234", card_number="1111-1111-1111-1111", 129 | expiry="2022-06", birth="960714", pwd_2digit="12") 130 | except errors.ImpApiError as e: 131 | self.assertEqual(e.response.code, -1) 132 | self.assertIn("유효하지않은 카드번호를 입력하셨습니다.", e.response.message) 133 | 134 | def test_find_billkey(self): 135 | self.assertRaises(KeyError, self.client.find_billkey, customer_uid=None) 136 | 137 | try: 138 | self.client.find_billkey(customer_uid="invalid-uid") 139 | except errors.ImpApiError as e: 140 | self.assertEqual(e.response.code, 1) 141 | self.assertEqual(e.response.message, "요청하신 customer_uid(invalid-uid)로 등록된 정보를 찾을 수 없습니다.") 142 | 143 | def test_delete_billkey(self): 144 | self.assertRaises(KeyError, self.client.delete_billkey, customer_uid=None) 145 | 146 | try: 147 | self.client.delete_billkey(customer_uid="invalid-uid") 148 | except errors.ImpApiError as e: 149 | self.assertEqual(e.response.code, 1) 150 | self.assertEqual(e.response.message, "요청하신 customer_uid(invalid-uid)로 등록된 정보를 찾을 수 없습니다.") 151 | 152 | def test_create_payment_onetime(self): 153 | self.assertRaises(KeyError, self.client.create_payment, merchant_uid=None, name="테스트", amount=10000) 154 | self.assertRaises(KeyError, self.client.create_payment, merchant_uid='some-special-uid', name="테스트", 155 | amount=10000, card_number="1111-1111-1111-1111") 156 | 157 | try: 158 | self.client.create_payment(merchant_uid='some-special-uid', amount=1000, name="테스트", 159 | card_number="1111-1111-1111-1111", expiry="2022-06", birth="960714") 160 | except errors.ImpApiError as e: 161 | self.assertEqual(e.response.code, -1) 162 | self.assertIn("유효하지않은 카드번호를 입력하셨습니다.", e.response.message) 163 | 164 | def test_create_payment_again(self): 165 | try: 166 | self.client.create_payment(merchant_uid='some-special-uid-2', amount=1000, name="테스트", 167 | customer_uid="invalid-uid", ) 168 | except errors.ImpApiError as e: 169 | self.assertEqual(e.response.code, 1) 170 | self.assertEqual(e.response.message, "등록되지 않은 구매자입니다.") 171 | 172 | def tearDown(self): 173 | del self.imp_auth 174 | del self.client 175 | 176 | 177 | if __name__ == "__main__": 178 | unittest.main() 179 | --------------------------------------------------------------------------------