diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..c0be67c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Publish Python Package + uses: JRubics/poetry-publish@v1.6 + with: + python_version: '3.8' + pypi_token: ${{ secrets.PYPI_TOKEN }} diff --git a/.secrets/.gitignore b/.secrets/.gitignore new file mode 100644 index 0000000..e76051e --- /dev/null +++ b/.secrets/.gitignore @@ -0,0 +1,6 @@ +# IMPORTANT! This folder is hidden from git - if you need to store config files or other secrets, +# make sure those are never staged for commit into your git repo. You can store them here or another +# secure location. + +* +!.gitignore diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 449c463..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE -include tap_teamwork/schemas/*.json diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..be46f87 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,576 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "backoff" +version = "1.8.0" +description = "Function decoration for backoff and retry" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "black" +version = "21.4b0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +python2 = ["typed-ast (>=1.4.2)"] + +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.5" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "ciso8601" +version = "2.1.3" +description = "Fast ISO8601 date time parser for Python written in C" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "cryptography" +version = "3.4.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "jsonschema" +version = "3.2.0" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0" +six = ">=1.11.0" + +[package.extras] +format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] +format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] + +[[package]] +name = "memoization" +version = "0.3.2" +description = "A powerful caching library for Python, with TTL support and multiple algorithm options. (https://github.com/lonelyenvoy/python-memoization)" +category = "main" +optional = false +python-versions = ">=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pathspec" +version = "0.8.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pendulum" +version = "1.5.1" +description = "Python datetimes made easy." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +python-dateutil = ">=2.6.0.0,<3.0.0.0" +pytzdata = ">=2018.3.0.0" +tzlocal = ">=1.5.0.0,<2.0.0.0" + +[[package]] +name = "pipelinewise-singer-python" +version = "1.2.0" +description = "Singer.io utility library - PipelineWise compatible" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +backoff = "1.8.0" +ciso8601 = "*" +jsonschema = "3.2.0" +python-dateutil = ">=2.6.0" +pytz = "<2021.0" +simplejson = "3.11.1" + +[package.extras] +dev = ["pylint", "ipython", "ipdb", "nose"] + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyjwt" +version = "1.7.1" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +crypto = ["cryptography (>=1.4)"] +flake8 = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] + +[[package]] +name = "pyrsistent" +version = "0.17.3" +description = "Persistent/Functional/Immutable data structures" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2020.5" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "regex" +version = "2021.4.4" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "simplejson" +version = "3.11.1" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "singer-sdk" +version = "0.1.3" +description = "A framework for building Singer taps" +category = "main" +optional = false +python-versions = ">=3.6,<3.9" + +[package.dependencies] +backoff = "1.8.0" +click = ">=7.1.2,<8.0.0" +cryptography = ">=3.4.6,<4.0.0" +memoization = ">=0.3.2,<0.4.0" +pendulum = ">=1.2.0,<2.0.0" +pipelinewise-singer-python = "1.2.0" +PyJWT = "1.7.1" +requests = ">=2.25.1,<3.0.0" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tzlocal" +version = "1.5.1" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytz = "*" + +[[package]] +name = "urllib3" +version = "1.26.4" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotlipy (>=0.6.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "3.8" +content-hash = "dde1179c2c4afbd923ba1bda9ccbe6c716cca57fa8cd8ee22b87dd3a571dada9" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +backoff = [ + {file = "backoff-1.8.0-py2.py3-none-any.whl", hash = "sha256:d340bb6f36d025c04214b8925112d8456970e5f28dda46e4f1133bf5c622cb0a"}, + {file = "backoff-1.8.0.tar.gz", hash = "sha256:c7187f15339e775aec926dc6e5e42f8a3ad7d3c2b9a6ecae7b535000f70cd838"}, +] +black = [ + {file = "black-21.4b0-py3-none-any.whl", hash = "sha256:2db7040bbbbaa46247bfcc05c6efdebd7ebe50c1c3ca745ca6e0f6776438c96c"}, + {file = "black-21.4b0.tar.gz", hash = "sha256:915d916c48646dbe8040d5265cff7111421a60a3dfe7f7e07273176a57c24a34"}, +] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +cffi = [ + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +ciso8601 = [ + {file = "ciso8601-2.1.3.tar.gz", hash = "sha256:bdbb5b366058b1c87735603b23060962c439ac9be66f1ae91e8c7dbd7d59e262"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +cryptography = [ + {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, + {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, + {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, + {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, + {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, + {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, + {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +jsonschema = [ + {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, + {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, +] +memoization = [ + {file = "memoization-0.3.2-py3-none-any.whl", hash = "sha256:6109bcfdbd6fc6c33004fcdc5d8e291c1223a7416c5dad61ec777d260f6038d2"}, + {file = "memoization-0.3.2.tar.gz", hash = "sha256:65d19404b9acc74a764d3e584d8fb17c56bc446d386a28afb93f2247507c99cc"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +pathspec = [ + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, +] +pendulum = [ + {file = "pendulum-1.5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:ddaf97a061eb5e2ae37857a8cb548e074125017855690d20e443ad8d9f31e164"}, + {file = "pendulum-1.5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c04fcf955e622e97e405e5f6d1b1f4a7adc69d79d82f3609643de69283170d6d"}, + {file = "pendulum-1.5.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:dd6500d27bb7ccc029d497da4f9bd09549bd3c0ea276dad894ea2fdf309e83f3"}, + {file = "pendulum-1.5.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:95536b33ae152e3c831eb236c1bf9ac9dcfb3b5b98fdbe8e9e601eab6c373897"}, + {file = "pendulum-1.5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:f4eee1e1735487d9d25cc435c519fd4380cb1f82cde3ebad1efbc2fc30deca5b"}, + {file = "pendulum-1.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:4173ce3e81ad0d9d61dbce86f4286c43a26a398270df6a0a89f501f0c28ad27d"}, + {file = "pendulum-1.5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:e9732b8bb214fad2c72ddcbfec07542effa8a8b704e174347ede1ff8dc679cce"}, + {file = "pendulum-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:56a347d0457859c84b8cdba161fc37c7df5db9b3becec7881cd770e9d2058b3c"}, + {file = "pendulum-1.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e7df37447824f9af0b58c7915a4caf349926036afd86ad38e7529a6b2f8fc34b"}, + {file = "pendulum-1.5.1.tar.gz", hash = "sha256:738878168eb26e5446da5d1f7b3312ae993a542061be8882099c00ef4866b1a2"}, +] +pipelinewise-singer-python = [ + {file = "pipelinewise-singer-python-1.2.0.tar.gz", hash = "sha256:8ba501f9092dbd686cd5792ecf6aa97c2d25c225e9d8b2875dcead0f5738898c"}, + {file = "pipelinewise_singer_python-1.2.0-py3-none-any.whl", hash = "sha256:156f011cba10b1591ae37c5510ed9d21639258c1377cc00c07d9f7e9a3ae27fb"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyjwt = [ + {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, + {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, +] +pyrsistent = [ + {file = "pyrsistent-0.17.3.tar.gz", hash = "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +pytz = [ + {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, + {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, +] +pytzdata = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] +regex = [ + {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, + {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, + {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, + {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, + {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, + {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, + {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, + {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, + {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, + {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, + {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, + {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, + {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, + {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, + {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, + {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, + {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, +] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] +simplejson = [ + {file = "simplejson-3.11.1-cp27-cp27m-win32.whl", hash = "sha256:38c2b563cd03363e7cb2bbba6c20ae4eaafd853a83954c8c8dd345ee391787bf"}, + {file = "simplejson-3.11.1-cp27-cp27m-win_amd64.whl", hash = "sha256:8d73b96a6ee7c81fd49dac7225e3846fd60b54a0b5b93a0aaea04c5a5d2e7bf2"}, + {file = "simplejson-3.11.1-cp33-cp33m-win32.whl", hash = "sha256:7f53ab6a675594f237ce7372c1edf742a6acb158149ed3259c5fffc5b613dc94"}, + {file = "simplejson-3.11.1-cp33-cp33m-win_amd64.whl", hash = "sha256:86aa9fd492230c4b8b6814fcf089b36ffba2cec4d0635c8c642135b9067ebbd7"}, + {file = "simplejson-3.11.1-cp34-cp34m-win32.whl", hash = "sha256:7df76ae6cac4a62ad5295f9a9131857077d84cb15fad2011acb2ce7410476009"}, + {file = "simplejson-3.11.1-cp34-cp34m-win_amd64.whl", hash = "sha256:a6939199c30b78ae31e62e6913f0e12cb71a4a5ad67c259e0a98688df027a5de"}, + {file = "simplejson-3.11.1-cp35-cp35m-win32.whl", hash = "sha256:11d91b88cc1e9645c79f0f6fd2961684249af963e2bbff5a00061ed4bbf55379"}, + {file = "simplejson-3.11.1-cp35-cp35m-win_amd64.whl", hash = "sha256:36b0de42e3a8a51086c339cc803f6ac7a9d1d5254066d680956a195ca12cf0d8"}, + {file = "simplejson-3.11.1.tar.gz", hash = "sha256:01a22d49ddd9a168b136f26cac87d9a335660ce07aa5c630b8e3607d6f4325e7"}, + {file = "simplejson-3.11.1.win-amd64-py2.7.exe", hash = "sha256:1975e6b621fe1c2b9321c56476e8ebe1b851006517c1d67041b378950374694c"}, + {file = "simplejson-3.11.1.win-amd64-py3.3.exe", hash = "sha256:f60f01b16215568a08611eb6a4d61d76c4173c3d69aac9cad593777056c284d5"}, + {file = "simplejson-3.11.1.win-amd64-py3.4.exe", hash = "sha256:6be48181337ac5f5d9f48c9c504f317e245519318992122a05c40e482a721d59"}, + {file = "simplejson-3.11.1.win-amd64-py3.5.exe", hash = "sha256:8ae8cdcbe49e29ddfdae0ab81c1f6c070706d18fcee86371352d0d54b47ad8ec"}, + {file = "simplejson-3.11.1.win32-py2.7.exe", hash = "sha256:ebbd52b59948350ad66205e66b299fcca0e0821ed275c21262c522f4a6cea9d2"}, + {file = "simplejson-3.11.1.win32-py3.3.exe", hash = "sha256:2dc7fb8c0c0ff9483ce31b93b700b1fa60aca9d099e6aca9813f28ff131ccf59"}, + {file = "simplejson-3.11.1.win32-py3.4.exe", hash = "sha256:97cc43ef4cb18a2725f6e26d22b96f8ca50872a195bde32707dcb284f89c1d4d"}, + {file = "simplejson-3.11.1.win32-py3.5.exe", hash = "sha256:c76d55d78dc8b06c96fd08c6cc5e2b0b650799627d3f9ca4ad23f40db72d5f6d"}, +] +singer-sdk = [ + {file = "singer-sdk-0.1.3.tar.gz", hash = "sha256:b638722e3411f6d0fe8056d92ccd469973a7257e9e12531c86c9c5396fedc45b"}, + {file = "singer_sdk-0.1.3-py3-none-any.whl", hash = "sha256:6dc447dee1dcc966436b126611cfc28fb298c3f93014123bb2c1df9a6a7c2c8d"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tzlocal = [ + {file = "tzlocal-1.5.1.tar.gz", hash = "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"}, +] +urllib3 = [ + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e0c95ba --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "tap-teamwork" +version = "0.3.1" +description = "Singer.io tap for Teamwork.com" +authors = ["Stephen Bailey "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/immuta/tap-teamwork" +keywords = [ + "singer", + "elt", + "replication" +] +classifiers=[ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Database" +] + + +[tool.poetry.dependencies] +python = "3.8" +singer-sdk = "^0.1.3" + +[tool.poetry.dev-dependencies] +black = "^21.4b0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2bd8572..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -singer-python>=5.9.0 -requests -black \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100755 index aec90d4..0000000 --- a/setup.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -import setuptools - -setuptools.setup( - name="tap-teamwork", - version="0.2.2", - description="Singer.io tap for extracting data", - author="Stephen Bailey", - url="http://singer.io", - classifiers=["Programming Language :: Python :: 3 :: Only"], - packages=setuptools.find_packages(), - py_modules=["tap_teamwork"], - package_data={"schemas": ["tap_teamwork/schemas/*.json"]}, - entry_points=""" - [console_scripts] - tap-teamwork=tap_teamwork:main - """, - install_requires=["singer-python", "requests"], - include_package_data=True, -) diff --git a/tap_teamwork/__init__.py b/tap_teamwork/__init__.py index 99f24d2..e69de29 100644 --- a/tap_teamwork/__init__.py +++ b/tap_teamwork/__init__.py @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 - -import singer - -from tap_teamwork.runner import Runner -from tap_teamwork.client import Client -from tap_teamwork.streams import AVAILABLE_STREAMS - -LOGGER = singer.get_logger() # noqa - - -@singer.utils.handle_top_exception(LOGGER) -def main(): - args = singer.utils.parse_args( - required_config_keys=["api_key", "hostname", "start_date"] - ) - client = Client(args.config) - runner = Runner(args, client, AVAILABLE_STREAMS) - - if args.discover: - runner.do_discover() - else: - runner.do_sync() - - -if __name__ == "__main__": - main() diff --git a/tap_teamwork/base.py b/tap_teamwork/base.py deleted file mode 100644 index 188c3ee..0000000 --- a/tap_teamwork/base.py +++ /dev/null @@ -1,255 +0,0 @@ -import inspect -import os.path -import pytz -import singer -import tap_teamwork.cache as stream_cache - -from datetime import timedelta, datetime - -from tap_teamwork.config import get_config_start_date -from tap_teamwork.state import incorporate, save_state, get_last_record_value_for_table - - -LOGGER = singer.get_logger() - - -class BaseStream: - # GLOBAL PROPERTIES - TABLE = None - RESPONSE_KEY = None - KEY_PROPERTIES = ["id"] - API_METHOD = "GET" - REQUIRES = [] - CACHE_RESULTS = False - path = "" - - def __init__(self, config, state, catalog, client): - self.config = config - self.state = state - self.catalog = catalog - self.client = client - self.substreams = [] - - def get_url_base(self): - if not self.config["hostname"].startswith("http"): - return ValueError("Hostname config should begin with 'https://'.") - return self.config["hostname"] + "/projects/api/v3/" - - def get_url(self): - base = self.get_url_base() - return f"{base}{self.path}" - - def get_params(self, page=1): - return {"updatedAfter": None, "page": page, "pageSize": 250} - - def get_class_path(self): - return os.path.dirname(inspect.getfile(self.__class__)) - - def load_schema_by_name(self, name): - return singer.utils.load_json( - os.path.normpath( - os.path.join(self.get_class_path(), "schemas/{}.json".format(name)) - ) - ) - - def get_schema(self): - return self.load_schema_by_name(self.TABLE) - - def get_stream_data(self, result): - """Given a result set, return the data - to be persisted for this stream. - """ - return [self.transform_record(record) for record in result] - - @classmethod - def requirements_met(cls, catalog): - selected_streams = [s.stream for s in catalog.streams if is_selected(s)] - - return set(cls.REQUIRES).issubset(selected_streams) - - @classmethod - def matches_catalog(cls, stream_catalog): - return stream_catalog.stream == cls.TABLE - - def generate_catalog(self, selected_by_default=True): - schema = self.get_schema() - mdata = singer.metadata.new() - - mdata = singer.metadata.write(mdata, (), "inclusion", "available") - mdata = singer.metadata.write( - mdata, (), "selected-by-default", selected_by_default - ) - - for field_name in schema.get("properties").keys(): - inclusion = "available" - - if field_name in self.KEY_PROPERTIES: - inclusion = "automatic" - - mdata = singer.metadata.write( - mdata, ("properties", field_name), "inclusion", inclusion - ) - mdata = singer.metadata.write( - mdata, - ("properties", field_name), - "selected-by-default", - selected_by_default, - ) - - return [ - { - "tap_stream_id": self.TABLE, - "stream": self.TABLE, - "key_properties": self.KEY_PROPERTIES, - "schema": self.get_schema(), - "metadata": singer.metadata.to_list(mdata), - } - ] - - def transform_record(self, record): - with singer.Transformer() as tx: - metadata = {} - - if self.catalog.metadata is not None: - metadata = singer.metadata.to_map(self.catalog.metadata) - - return tx.transform(record, self.catalog.schema.to_dict(), metadata) - - def get_catalog_keys(self): - return list(self.catalog.schema.properties.keys()) - - def write_schema(self): - singer.write_schema( - self.catalog.stream, - self.catalog.schema.to_dict(), - key_properties=self.catalog.key_properties, - ) - - def sync(self): - LOGGER.info( - "Syncing stream {} with {}".format( - self.catalog.tap_stream_id, self.__class__.__name__ - ) - ) - - self.write_schema() - - return self.sync_data() - - def sync_data(self): - table = self.TABLE - - LOGGER.info("Syncing data for %s", table) - - url = self.get_url() - params = self.get_params() - resources = self.sync_paginated(url, params) - - if self.CACHE_RESULTS: - stream_cache.add(table, resources) - LOGGER.info("Added %s %s to cache", len(resources), table) - - LOGGER.info("Reached end of stream, moving on.") - save_state(self.state) - return self.state - - def sync_paginated(self, url, params): - table = self.TABLE - _next = True - page = 1 - - all_resources = [] - # teamwork returns an array of data, without pagination metadata - # Should iterate if number of results > page size - while _next is not None: - result = self.client.make_request(url, self.API_METHOD, params=params) - result_records = result.get(self.RESPONSE_KEY) - data = self.get_stream_data(result_records) - - with singer.metrics.record_counter(endpoint=table) as counter: - singer.write_records(table, data) - counter.increment(len(data)) - all_resources.extend(data) - - LOGGER.info("Synced page %s for %s", page, self.TABLE) - params["page"] = params["page"] + 1 - if len(data) < params.get("pageSize", 250): - _next = None - return all_resources - - -def is_selected(stream_catalog): - metadata = singer.metadata.to_map(stream_catalog.metadata) - stream_metadata = metadata.get((), {}) - - inclusion = stream_metadata.get("inclusion") - - if stream_metadata.get("selected") is not None: - selected = stream_metadata.get("selected") - else: - selected = stream_metadata.get("selected-by-default") - - if inclusion == "unsupported": - return False - - elif selected is not None: - return selected - - return inclusion == "automatic" - - -class TimeRangeByObjectStream(BaseStream): - """Stream that retrieves data based on a time range, and - iterates over a certain dimension, such as or - . - """ - - RANGE_FIELD = "updatedAfter" - REPLACEMENT_STRING = "" - - def get_params(self, start, end, page=1): - return { - self.RANGE_FIELD: start.strftime("%Y-%m-%dT%H:%M:%SZ"), - "page": 1, - "pageSize": 250, - } - - def get_object_list(self): - return [] - - def sync_data(self): - table = self.TABLE - object_list = self.get_object_list() - - start_date = get_last_record_value_for_table(self.state, table) - if start_date is None: - start_date = get_config_start_date(self.config) - end_date = datetime.now(pytz.utc) - - all_resources = self.sync_data_for_period(start_date, end_date, object_list) - - if self.CACHE_RESULTS: - stream_cache.add(table, all_resources) - LOGGER.info("Added %s %s to cache", len(all_resources), table) - - return self.state - - def sync_data_for_period(self, start_date, end_date, object_list): - table = self.TABLE - - LOGGER.info( - "Syncing data from %s to %s", start_date.isoformat(), end_date.isoformat() - ) - - res = None - for obj in object_list: - params = self.get_params(start_date, end_date) - url = self.get_url().replace(self.REPLACEMENT_STRING, obj) - res = self.sync_paginated(url, params) - - self.state = incorporate( - self.state, table, self.RANGE_FIELD, end_date.isoformat() - ) - - save_state(self.state) - return res diff --git a/tap_teamwork/client.py b/tap_teamwork/client.py deleted file mode 100644 index afe9f22..0000000 --- a/tap_teamwork/client.py +++ /dev/null @@ -1,31 +0,0 @@ -import requests -import singer - -LOGGER = singer.get_logger() # noqa - - -class Client: - - MAX_TRIES = 5 - - def __init__(self, config): - self.config = config - - def make_request(self, url, method, params=None, body=None): - LOGGER.info("Making %s request to %s (%s)", method, url, params) - - # Basic Auth requires API Key as user with any password string - auth = requests.auth.HTTPBasicAuth(self.config["api_key"], "xxx") - response = requests.request( - method, - url, - headers={"Content-Type": "application/json"}, - auth=auth, - params=params, - json=body, - ) - - if response.status_code != 200: - raise RuntimeError(response.text) - - return response.json() diff --git a/tap_teamwork/config.py b/tap_teamwork/config.py deleted file mode 100644 index 630b036..0000000 --- a/tap_teamwork/config.py +++ /dev/null @@ -1,9 +0,0 @@ -import singer - -from dateutil.parser import parse - -LOGGER = singer.get_logger() - - -def get_config_start_date(config): - return parse(config.get("start_date")) diff --git a/tap_teamwork/runner.py b/tap_teamwork/runner.py deleted file mode 100644 index df86ddf..0000000 --- a/tap_teamwork/runner.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -import singer -import sys - -from tap_teamwork.state import save_state - - -LOGGER = singer.get_logger() - - -class Runner: - def __init__(self, args, client, available_streams): - self.available_streams = available_streams - self.catalog = args.catalog - self.client = client - self.config = args.config - self.state = args.state - - def get_streams_to_replicate(self): - streams = [] - - if not self.catalog: - return streams - - for stream_catalog in self.catalog.streams: - if not is_selected(stream_catalog): - LOGGER.info( - "'%s' is not marked selected, skipping.", stream_catalog.stream - ) - continue - - for available_stream in self.available_streams: - if available_stream.matches_catalog(stream_catalog): - if not available_stream.requirements_met(self.catalog): - raise RuntimeError( - "%s requires that that the following are selected: %s", - stream_catalog.stream, - ",".join(available_stream.REQUIRES), - ) - - to_add = available_stream( - self.config, self.state, stream_catalog, self.client - ) - - streams.append(to_add) - - return streams - - def do_discover(self): - LOGGER.info("Starting discovery.") - - catalog = [] - - for available_stream in self.available_streams: - stream = available_stream(self.config, self.state, None, None) - - catalog += stream.generate_catalog() - - json.dump({"streams": catalog}, sys.stdout, indent=4) - - def do_sync(self): - LOGGER.info("Starting sync.") - - streams = self.get_streams_to_replicate() - - for stream in streams: - try: - stream.state = self.state - stream.sync() - self.state = stream.state - except OSError as e: - LOGGER.error(str(e)) - exit(e.errno) - - except Exception as e: - LOGGER.error(str(e)) - LOGGER.error("Failed to sync endpoint %s, moving on!", stream.TABLE) - raise e - - save_state(self.state) - - -def is_selected(stream_catalog): - metadata = singer.metadata.to_map(stream_catalog.metadata) - stream_metadata = metadata.get((), {}) - - inclusion = stream_metadata.get("inclusion") - - if stream_metadata.get("selected") is not None: - selected = stream_metadata.get("selected") - else: - selected = stream_metadata.get("selected-by-default") - - if inclusion == "unsupported": - return False - - elif selected is not None: - return selected - - return inclusion == "automatic" diff --git a/tap_teamwork/state.py b/tap_teamwork/state.py deleted file mode 100644 index 405c107..0000000 --- a/tap_teamwork/state.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -import singer - -from dateutil.parser import parse - -LOGGER = singer.get_logger() - - -def get_last_record_value_for_table(state, table): - last_value = singer.bookmarks.get_bookmark(state, table, "last_record") - - if last_value is None: - return None - - return parse(last_value) - - -def incorporate(state, table, field, value): - if value is None: - return state - - new_state = state.copy() - - parsed = parse(value).strftime("%Y-%m-%dT%H:%M:%SZ") - - if "bookmarks" not in new_state: - new_state["bookmarks"] = {} - - last_record = singer.bookmarks.get_bookmark(new_state, table, "last_record") - if last_record is None or last_record < value: - new_state = singer.bookmarks.write_bookmark(new_state, table, field, parsed) - - return new_state - - -def save_state(state): - if not state: - return - - LOGGER.info("Updating state.") - - singer.write_state(state) - - -def load_state(filename): - if filename is None: - return {} - - try: - return singer.utils.load_json(filename) - except: - LOGGER.fatal("Failed to decode state file. Is it valid json?") - raise RuntimeError diff --git a/tap_teamwork/streams.py b/tap_teamwork/streams.py index 95e43b9..fc32a01 100644 --- a/tap_teamwork/streams.py +++ b/tap_teamwork/streams.py @@ -1,204 +1,218 @@ -import singer +"""Stream class for tap-teamwork.""" -from tap_teamwork.base import BaseStream, TimeRangeByObjectStream +import requests +from base64 import b64encode +from pathlib import Path +from typing import Any, Dict, Optional, Union, List, Iterable +from singer_sdk.streams import RESTStream -LOGGER = singer.get_logger() +SCHEMAS_DIR = Path(__file__).parent / Path("./schemas") -class CompaniesStream(BaseStream): - TABLE = "companies" - RESPONSE_KEY = "companies" - CACHE_RESULTS = True +class TeamworkStream(RESTStream): + """Teamwork stream class.""" - @property - def path(self): - return f"companies.json" - - # The 'companies' endpoint is only in API version 1, so requires a different base - def get_url_base(self): - return self.config["hostname"] + "/" - - -class LatestActivityStream(BaseStream): - TABLE = "latest_activity" - RESPONSE_KEY = "activities" - - CACHE_RESULTS = True - - @property - def path(self): - return f"latestactivity.json" - - -class MilestonesStream(BaseStream): - TABLE = "milestones" - RESPONSE_KEY = "milestones" - - CACHE_RESULTS = True - - @property - def path(self): - return "milestones.json" - - -class ProjectsStream(BaseStream): - TABLE = "projects" - RESPONSE_KEY = "projects" + response_result_key = None + _page_size = 250 @property - def path(self): - return f"projects.json" + def http_headers(self) -> dict: + "Implement Basic Auth with API Key as username and dummy password" + result = super().http_headers + api_key = self.config.get("api_key") + auth = b64encode(f"{api_key}:xxx".encode()).decode() -class ProjectCustomFieldsStream(BaseStream): - TABLE = "project_custom_fields" - RESPONSE_KEY = "included" + result["Authorization"] = f"Basic {auth}" - CACHE_RESULTS = True + return result @property - def path(self): - return "projects.json" - - def get_params(self, page=1): - return { + def url_base(self) -> str: + """Return the API URL root, configurable via tap settings.""" + return self.config["hostname"] + "/projects/api/v3/" + + def get_url_params( + self, partition: Optional[dict], next_page_token: Optional[Any] = 1 + ) -> Dict[str, Any]: + params = { "updatedAfter": None, - "page": page, - "pageSize": 250, - "includeCustomFields": True, - "fields[customfields]": '[id,entity,name,description,type]' + "page": next_page_token, + "pageSize": self._page_size, } + return params + + def parse_response(self, response: requests.Response) -> Iterable[dict]: + """Parse the response and return an iterator of result rows.""" + resp_json = response.json() + if self.response_result_key: + resp_json = resp_json.get(self.response_result_key, {}) + if isinstance(resp_json, dict): + yield resp_json + else: + for row in resp_json: + yield row + + def get_next_page_token( + self, + response: requests.Response, + previous_token: Union[int, None], + ) -> Union[int, None]: + + previous_token = previous_token or 0 + data = response.json() + results = data.get(self.response_result_key, data) + + if len(results) >= self._page_size: + return previous_token + 1 + + return None + + +class CompaniesStream(TeamworkStream): + name = "companies" + path = "/companies.json" + primary_keys = ["id"] + response_result_key = "companies" + schema_filepath = SCHEMAS_DIR / "companies.json" - def sync_paginated(self, url, params): - table = self.TABLE - _next = True - page = 1 + @property + def url_base(self) -> str: + """The 'companies' endpoint is only in API version 1, so requires a different base""" + return self.config["hostname"] + "/" - all_resources = [] - while _next is not None: - result = self.client.make_request(url, self.API_METHOD, params=params) - custom_fields = result.get("included", {}).get("customfields", {}) - raw_records = result.get("included", {}).get("customfieldProjects", {}) - proc_records = [] - for k, v in raw_records.items(): - combined = {**v, **custom_fields[str(v.get("customfieldId"))]} - proc_records.append(combined) - data = self.get_stream_data(proc_records) +class LatestActivityStream(TeamworkStream): + name = "latest_activity" + path = "latestactivity.json" + primary_keys = ["id"] + response_result_key = "activities" + schema_filepath = SCHEMAS_DIR / "latest_activity.json" - with singer.metrics.record_counter(endpoint=table) as counter: - singer.write_records(table, data) - counter.increment(len(data)) - all_resources.extend(data) - LOGGER.info("Synced page %s for %s", page, self.TABLE) - params["page"] = params["page"] + 1 - if len(data) < params.get("pageSize", 250): - _next = None - return all_resources +class MilestonesStream(TeamworkStream): + name = "milestones" + path = "milestones.json" + response_result_key = "milestones" + primary_keys = ["id"] + schema_filepath = SCHEMAS_DIR / "milestones.json" -class PeopleStream(BaseStream): - TABLE = "people" - RESPONSE_KEY = "people" - CACHE_RESULTS = True +class PeopleStream(TeamworkStream): + name = "people" + path = "people.json" + primary_keys = ["id"] + response_result_key = "people" + schema_filepath = SCHEMAS_DIR / "people.json" - @property - def path(self): - return f"people.json" +class ProjectsStream(TeamworkStream): + name = "projects" + path = "projects.json" + primary_keys = ["id"] + response_result_key = "projects" + schema_filepath = SCHEMAS_DIR / "projects.json" -class ProjectUpdatesStream(BaseStream): - TABLE = "project_updates" - RESPONSE_KEY = "projectUpdates" + def get_url_params(self, partition, next_page_token=None): + return { + "includeArchivedProjects": True, + "page": next_page_token, + "pageSize": self._page_size, + } - CACHE_RESULTS = True - @property - def path(self): - return "projects/updates.json" +class ProjectCustomFieldsStream(TeamworkStream): + name = "project_custom_fields" + path = "projects.json" + primary_keys = ["id"] + response_result_key = "included" + schema_filepath = SCHEMAS_DIR / "project_custom_fields.json" + + def get_url_params(self, partition, next_page_token=None): + return { + "includeArchivedProjects": True, + "fields[customfields]": "[id,entity,name,description,type]", + "includeCustomFields": True, + "page": next_page_token, + "pageSize": self._page_size, + } + def parse_response(self, response: requests.Response) -> Iterable[dict]: + """Parse the response and return an iterator of result rows.""" + resp_json = response.json() -class RisksStream(BaseStream): - TABLE = "risks" - RESPONSE_KEY = "risks" + # Extract custom fields + custom_fields = resp_json.get("included", {}).get("customfields", {}) + raw_records = resp_json.get("included", {}).get("customfieldProjects", {}) - CACHE_RESULTS = True + for k, v in raw_records.items(): + merged = {**v, **custom_fields[str(v.get("customfieldId"))]} + yield merged - @property - def path(self): - return f"risks.json" +class ProjectUpdatesStream(TeamworkStream): + name = "project_updates" + path = "projects/updates.json" + primary_keys = ["id"] + response_result_key = "projectUpdates" + schema_filepath = SCHEMAS_DIR / "project_updates.json" -class TagsStream(BaseStream): - TABLE = "tags" - RESPONSE_KEY = "tags" - CACHE_RESULTS = True +class RisksStream(TeamworkStream): + name = "risks" + path = "risks.json" + primary_keys = ["id"] + response_result_key = "risks" + schema_filepath = SCHEMAS_DIR / "risks.json" - @property - def path(self): - return f"tags.json" +class TagsStream(TeamworkStream): + name = "tags" + path = "tags.json" + primary_keys = ["id"] + response_result_key = "tags" + schema_filepath = SCHEMAS_DIR / "tags.json" -class TasksStream(BaseStream): - TABLE = "tasks" - RESPONSE_KEY = "todo-items" - CACHE_RESULTS = True +class TasksStream(TeamworkStream): + name = "tasks" + path = "tasks.json" + primary_keys = ["id"] + response_result_key = "todo-items" + schema_filepath = SCHEMAS_DIR / "tasks.json" @property - def path(self): - return f"tasks.json" + def url_base(self) -> str: + """The 'tasks' endpoint is only in API version 1, so requires a different base""" + return self.config["hostname"] + "/" - def get_params(self, page=1): + def get_url_params(self, partition, next_page_token=None): return { "updatedAfter": None, - "page": page, - "pageSize": 250, - "includeCompletedTasks": True + "page": next_page_token, + "pageSize": self._page_size, + "includeCompletedTasks": True, } - # The 'tasks' endpoint is only in API version 1, so requires a different base - def get_url_base(self): - return self.config["hostname"] + "/" - -class CategoriesStream(BaseStream): - TABLE = "categories" - RESPONSE_KEY = "categories" - - CACHE_RESULTS = True +class CategoriesStream(TeamworkStream): + name = "categories" + path = "projectCategories.json" + primary_keys = ["id"] + response_result_key = "categories" + schema_filepath = SCHEMAS_DIR / "categories.json" @property - def path(self): - return f"projectCategories.json" + def url_base(self) -> str: + """The 'catgories' endpoint is only in API version 1, so requires a different base""" + return self.config["hostname"] + "/" - def get_params(self, page=1): + def get_url_params(self, partition, next_page_token=None): return { "updatedAfter": None, - "page": page, - "pageSize": 250, + "page": next_page_token, + "pageSize": self._page_size, } - - # The 'tasks' endpoint is only in API version 1, so requires a different base - def get_url_base(self): - return self.config["hostname"] + "/" - - -AVAILABLE_STREAMS = [ - CategoriesStream, - CompaniesStream, - LatestActivityStream, - ProjectsStream, - ProjectCustomFieldsStream, - ProjectUpdatesStream, - PeopleStream, - MilestonesStream, - RisksStream, - TagsStream, - TasksStream, -] diff --git a/tap_teamwork/tap.py b/tap_teamwork/tap.py new file mode 100644 index 0000000..6afd182 --- /dev/null +++ b/tap_teamwork/tap.py @@ -0,0 +1,63 @@ +"""Teamwork tap class.""" + +from pathlib import Path +from typing import List + +from singer_sdk import Tap, Stream +from singer_sdk.typing import ( + DateTimeType, + PropertiesList, + Property, + StringType, +) + +from tap_teamwork.streams import ( + CategoriesStream, + CompaniesStream, + LatestActivityStream, + ProjectsStream, + ProjectCustomFieldsStream, + ProjectUpdatesStream, + PeopleStream, + MilestonesStream, + RisksStream, + TagsStream, + TasksStream, +) + + +STREAM_TYPES = [ + CategoriesStream, + CompaniesStream, + LatestActivityStream, + ProjectsStream, + ProjectCustomFieldsStream, + ProjectUpdatesStream, + PeopleStream, + MilestonesStream, + RisksStream, + TagsStream, + TasksStream, +] + + +class TapTeamwork(Tap): + """Teamwork tap class.""" + + name = "tap-teamwork" + + config_jsonschema = PropertiesList( + Property("api_key", StringType, required=True), + Property("hostname", StringType, required=True), + Property("start_date", DateTimeType), + Property("user_agent", StringType, default="tap-teamwork@teamwork.com"), + ).to_dict() + + def discover_streams(self) -> List[Stream]: + """Return a list of discovered streams.""" + return [stream_class(tap=self) for stream_class in STREAM_TYPES] + + +# CLI Execution: + +cli = TapTeamwork.cli diff --git a/tap_teamwork/tests/__init__.py b/tap_teamwork/tests/__init__.py new file mode 100644 index 0000000..dbb2fba --- /dev/null +++ b/tap_teamwork/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for tap-teamwork.""" diff --git a/tap_teamwork/tests/test_core.py b/tap_teamwork/tests/test_core.py new file mode 100644 index 0000000..8d1a2f0 --- /dev/null +++ b/tap_teamwork/tests/test_core.py @@ -0,0 +1,21 @@ +"""Tests standard tap features using the built-in SDK tests library.""" + +import datetime + +from singer_sdk.testing import get_standard_tap_tests +from tap_teamwork.tap import TapTeamwork + + +SAMPLE_CONFIG = { + "api_key": "my_api_key", + "hostname": "https://company.teamwork.com", + "start_date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d"), +} + + +# Run standard built-in tap tests from the SDK: +def test_standard_tap_tests(): + """Run standard tap tests from the SDK.""" + tests = get_standard_tap_tests(TapTeamwork, config=SAMPLE_CONFIG) + for test in tests: + test()