diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d4a2c44 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[Makefile] +indent_style = tab diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..1c441c6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +* pyteamsnap version: +* Python version: +* Operating System: + +### Description + +Describe what you were trying to get done. +Tell us what happened, what went wrong, and what you expected to happen. + +### What I Did + +``` +Paste the command(s) you ran and the output. +If there was a crash, please include the traceback here. +``` diff --git a/.gitignore b/.gitignore index 08f86ac..4c915d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,15 @@ -# Backup files # -*.bak - -# If you are using PyCharm # -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/gradle.xml -.idea/**/libraries -*.iws /out/ -.idea/ - -# Python # +# Byte-compiled / optimized / DLL files +__pycache__/ *.py[cod] *$py.class +# C extensions +*.so + # Distribution / packaging -.Python build/ +.Python +env/ +build/ develop-eggs/ dist/ downloads/ @@ -37,6 +24,10 @@ wheels/ *.egg-info/ .installed.cfg *.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -50,29 +41,59 @@ htmlcov/ .coverage .coverage.* .cache -.pytest_cache/ nosetests.xml coverage.xml *.cover .hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints # pyenv .python-version -# celery -celerybeat-schedule.* +# celery beat schedule file +celerybeat-schedule # SageMath parsed files *.sage.py -# Environments +# dotenv .env + +# virtualenv .venv -env/ venv/ ENV/ -env.bak/ -venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject # mkdocs documentation /site @@ -80,4 +101,6 @@ venv.bak/ # mypy .mypy_cache/ -tests/private_test.py \ No newline at end of file +# IDE settings +.vscode/ +.idea/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aaba487 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +# Config file for automatic testing at travis-ci.com + +language: python +python: + - 3.8 + - 3.7 + - 3.6 + +# Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors +install: pip install -U tox-travis + +# Command to run tests, e.g. python setup.py test +script: tox + + diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..8392077 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,13 @@ +======= +Credits +======= + +Development Lead +---------------- + +* Anthony Correa + +Contributors +------------ + +None yet. Why not be the first? diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..8b7610f --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,128 @@ +.. highlight:: shell + +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/anthonyscorrea/pyteamsnap/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +pyteamsnap could always use more documentation, whether as part of the +official pyteamsnap docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at https://github.com/anthonyscorrea/pyteamsnap/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `pyteamsnap` for local development. + +1. Fork the `pyteamsnap` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/pyteamsnap.git + +3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: + + $ mkvirtualenv pyteamsnap + $ cd pyteamsnap/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass flake8 and the + tests, including testing other Python versions with tox:: + + $ flake8 pyteamsnap tests + $ python setup.py test or pytest + $ tox + + To get flake8 and tox, just pip install them into your virtualenv. + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.rst. +3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check + https://travis-ci.com/anthonyscorrea/pyteamsnap/pull_requests + and make sure that the tests pass for all supported Python versions. + +Tips +---- + +To run a subset of tests:: + + + $ python -m unittest tests.test_pyteamsnap + +Deploying +--------- + +A reminder for the maintainers on how to deploy. +Make sure all your changes are committed (including an entry in HISTORY.rst). +Then run:: + +$ bump2version patch # possible: major / minor / patch +$ git push +$ git push --tags + +Travis will then deploy to PyPI if tests pass. diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..2131650 --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,8 @@ +======= +History +======= + +0.1.0 (2022-06-20) +------------------ + +* First release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6a3c28 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2022, Anthony Correa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..965b2dd --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +include AUTHORS.rst +include CONTRIBUTING.rst +include HISTORY.rst +include LICENSE +include README.rst + +recursive-include tests * +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] + +recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f44b54c --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +.PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 lint/black +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +lint/flake8: ## check style with flake8 + flake8 pyteamsnap tests +lint/black: ## check style with black + black --check pyteamsnap tests + +lint: lint/flake8 lint/black ## check style + +test: ## run tests quickly with the default Python + python setup.py test + +test-all: ## run tests on every Python version with tox + tox + +coverage: ## check code coverage quickly with the default Python + coverage run --source pyteamsnap setup.py test + coverage report -m + coverage html + $(BROWSER) htmlcov/index.html + +docs: ## generate Sphinx HTML documentation, including API docs + rm -f docs/pyteamsnap.rst + rm -f docs/modules.rst + sphinx-apidoc -o docs/ pyteamsnap + $(MAKE) -C docs clean + $(MAKE) -C docs html + $(BROWSER) docs/_build/html/index.html + +servedocs: docs ## compile the docs watching for changes + watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source and wheel package + python setup.py sdist + python setup.py bdist_wheel + ls -l dist + +install: clean ## install the package to the active Python's site-packages + python setup.py install diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..86a96ec --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = pyteamsnap +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..f355d92 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# +# pyteamsnap documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 13:47:02 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another +# directory, add these directories to sys.path here. If the directory is +# relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +import pyteamsnap + +# -- General configuration --------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'pyteamsnap' +copyright = "2022, Anthony Correa" +author = "Anthony Correa" + +# The version info for the project you're documenting, acts as replacement +# for |version| and |release|, also used in various other places throughout +# the built documents. +# +# The short X.Y version. +version = pyteamsnap.__version__ +# The full version, including alpha/beta/rc tags. +release = pyteamsnap.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a +# theme further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output --------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyteamsnapdoc' + + +# -- Options for LaTeX output ------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'pyteamsnap.tex', + 'pyteamsnap Documentation', + 'Anthony Correa', 'manual'), +] + + +# -- Options for manual page output ------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pyteamsnap', + 'pyteamsnap Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'pyteamsnap', + 'pyteamsnap Documentation', + author, + 'pyteamsnap', + 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 0000000..2506499 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1 @@ +.. include:: ../HISTORY.rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7f4cf5d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +Welcome to pyteamsnap's documentation! +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + readme + installation + usage + modules + contributing + authors + history + +Indices and tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..61c8ac5 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,51 @@ +.. highlight:: shell + +============ +Installation +============ + + +Stable release +-------------- + +To install pyteamsnap, run this command in your terminal: + +.. code-block:: console + + $ pip install pyteamsnap + +This is the preferred method to install pyteamsnap, as it will always install the most recent stable release. + +If you don't have `pip`_ installed, this `Python installation guide`_ can guide +you through the process. + +.. _pip: https://pip.pypa.io +.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ + + +From sources +------------ + +The sources for pyteamsnap can be downloaded from the `Github repo`_. + +You can either clone the public repository: + +.. code-block:: console + + $ git clone git://github.com/anthonyscorrea/pyteamsnap + +Or download the `tarball`_: + +.. code-block:: console + + $ curl -OJL https://github.com/anthonyscorrea/pyteamsnap/tarball/master + +Once you have a copy of the source, you can install it with: + +.. code-block:: console + + $ python setup.py install + + +.. _Github repo: https://github.com/anthonyscorrea/pyteamsnap +.. _tarball: https://github.com/anthonyscorrea/pyteamsnap/tarball/master diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..d074abc --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=python -msphinx +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=pyteamsnap + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The Sphinx module was not found. Make sure you have Sphinx installed, + echo.then set the SPHINXBUILD environment variable to point to the full + echo.path of the 'sphinx-build' executable. Alternatively you may add the + echo.Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..72a3355 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..177c2cb --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +To use pyteamsnap in a project:: + + import pyteamsnap diff --git a/pyteamsnap/__init__.py b/pyteamsnap/__init__.py index 31d7a75..4acbc20 100644 --- a/pyteamsnap/__init__.py +++ b/pyteamsnap/__init__.py @@ -1,3 +1,5 @@ -from .api import TeamSnap +"""Top-level package for pyteamsnap.""" -__all__ = ['TeamSnap'] \ No newline at end of file +__author__ = """Anthony Correa""" +__email__ = 'a@correa.co' +__version__ = '0.1.0' diff --git a/pyteamsnap/client.py b/pyteamsnap/client.py new file mode 100644 index 0000000..ebe20e3 --- /dev/null +++ b/pyteamsnap/client.py @@ -0,0 +1,115 @@ +from apiclient import APIClient, HeaderAuthentication, JsonResponseHandler, JsonRequestFormatter +import datetime + +class TeamSnap(APIClient): + base_url = 'https://api.teamsnap.com/v3' + + def __init__(self, token, *args, **kwargs): + super().__init__(*args, + authentication_method=HeaderAuthentication(token=token), + response_handler=JsonResponseHandler, + request_formatter=JsonRequestFormatter, + **kwargs) + self._root_collection = self.get(self.base_url)['collection'] + self._links = self._by_rel(self.base_url, 'links') + self._queries = self._by_rel(self.base_url, 'queries') + self._commands = self._by_rel(self.base_url, 'commands') + pass + + def link(self, link_name): + d = {l['rel']:l['href'] for l in self._root_collection["links"]} + return d.get(link_name) + + def bulk_load(self, team_id, types, **kwargs): + """ + Returns a heterogeneous collection of the specified types for a specified team or teams. + Additional filters can be passed into requested types by passing them in the url's querystring + as type__filter=value (i.e. ?event__start_date=2015-01-01). + Any filter can be passed that is available on the search for the specified type. + :param team_id: + :param types: + :param kwargs: + :return: + """ + types_dict = {t.type:t for t in types} + r = self.query( + rel="self", + query="bulk_load", + types=",".join(types_dict.keys()), + team_id=team_id, + **kwargs + ) + + result = [] + for item in r: + cls = types_dict[item['type']] + instance = cls(self, rel=cls.rel, data=item) + result.append(instance) + return result + + def _by_rel (self, url, k): + # try: + # return {l['rel']: l for l in self._root_collection[k]} + # except Exception as e: + # return {} + # self.get(url)['collection'][k] + return {l['rel']:l for l in self.get(url)['collection'][k]} + + def query (self, rel, query, **kwargs): + queries = self._by_rel(self._get_href(rel), 'queries') + response = self.get(self._get_href(rel=query, links=queries), params=kwargs) + return self.parse_response(response) + + def command (self, rel, command, **kwargs): + commands = self._by_rel(self._get_href(rel), 'commands') + response = self.get(self._get_href(command, commands), params=kwargs) + return self.parse_response(response) + + def _get_href (self, rel: str, links:dict = None, url = base_url) -> str: + """returns a hyperlink from a the links dictionary. Each item in the links dictionary is a + dictionary with a rel and href key""" + try: + if links is None: links = self._by_rel(url, 'links') + + link = links[rel]['href'] + except Exception as e: + pass + return link + + def get_item (self, rel, id): + r = self.get(f"{self.link(rel)}/{id}") + return self.parse_response(r)[0] + + def post_item(self, rel, data): + r = super(TeamSnap, self).post(f"{self.link(rel)}", data=data) + return self.parse_response(r)[0] + + def put_item(self, rel, id, data): + r = super(TeamSnap, self).put(f"{self.link(rel)}/{id}", data=data) + return self.parse_response(r)[0] + + def delete_item(self, rel, id): + r = super(TeamSnap, self).delete(f"{self.link(rel)}/{id}") + return None + + @classmethod + def parse_response(self, response): + result = [] + items = [item['data'] for item in response['collection'].get('items',[])] + for item in response['collection'].get('items',[]): + details = {} + for detail in item['data']: + value = detail['value'] + value_type = detail['type'] + if value: + if value_type == 'DateTime': + value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z') + elif value_type == 'Boolean': + value = value == True + elif value_type == 'Integer': + value = int(value) + details[detail['name']] = value + result.append(details) + + return result + # return [{detail['name']: detail['value'] for detail in item} for item in items] diff --git a/pyteamsnap/objects.py b/pyteamsnap/objects.py new file mode 100644 index 0000000..25d82f7 --- /dev/null +++ b/pyteamsnap/objects.py @@ -0,0 +1,627 @@ +import apiclient.exceptions +from apiclient import APIClient, HeaderAuthentication, JsonResponseHandler, JsonRequestFormatter + + +class ApiObject(): + rel = None + version = None + template = None + + def __init__(self, client, rel=rel, data={}): + self.client = client + self.data = data + self.rel = rel + + @classmethod + def search(cls, client, **kwargs): + try: + results = client.query(cls.rel, "search", **kwargs) + except apiclient.exceptions.ServerError as e: + raise e + return [cls(client,rel=cls.rel, data=r) for r in results] + + @classmethod + def get(cls, client, id): + r = client.get(f"{client.link(cls.rel)}/{id}") + return cls(client, cls.rel, client.parse_response(r)[0]) + + @classmethod + def new(cls, client): + return cls(client, cls.rel) + + def post(self): + data = { + "template":{ + "data": [{ + "name":k, + "value":v + } for k,v in self.data.items()] + } + } + r = self.client.post_item(self.rel, data=data) + self.data = r + return r + + def put(self): + data = { + "template":{ + "data": [{ + "name":k, + "value":str(v) + } for k,v in self.data.items()] + } + } + id = self.data.get('id') + r = self.client.put_item(self.rel, id=id, data=data) + self.data = r + return r + + def delete(self): + self.client.delete_item(self.rel, id=self.data['id']) + +class Me (ApiObject): + rel = "me" + type = "user" + version = "3.866.0" + template = { + "data": [ + { + "name": "first_name" + }, + { + "name": "last_name" + }, + { + "name": "password" + }, + { + "name": "birthday" + }, + { + "name": "email" + }, + { + "name": "facebook_id", + "deprecated": True, + "prompt": "facebook_id is deprecated and has been removed. Continued use of facebook_id is not recommended it will no longer be stored." + }, + { + "name": "facebook_access_token", + "deprecated": True, + "prompt": "facebook_access_token is deprecated and has been removed. Continued use of facebook_access_token is not recommended it will no longer be stored." + }, + { + "name": "type", + "value": "user" + }, + { + "name": "is_lab_rat" + }, + { + "name": "receives_newsletter" + } + ] + } + + def __init__(self, client): + data = client.parse_response(client.get(client.link(self.rel)))[0] + super().__init__(client=client, rel=self.rel, data=data) + + +class User (ApiObject): + rel = "users" + type = "user" + version = "3.866.0" + template = { + "data": [ + { + "name": "first_name" + }, + { + "name": "last_name" + }, + { + "name": "password" + }, + { + "name": "birthday" + }, + { + "name": "email" + }, + { + "name": "facebook_id", + "deprecated": True, + "prompt": "facebook_id is deprecated and has been removed. Continued use of facebook_id is not recommended it will no longer be stored." + }, + { + "name": "facebook_access_token", + "deprecated": True, + "prompt": "facebook_access_token is deprecated and has been removed. Continued use of facebook_access_token is not recommended it will no longer be stored." + }, + { + "name": "type", + "value": "user" + }, + { + "name": "is_lab_rat" + }, + { + "name": "receives_newsletter" + } + ] + } + +class Event (ApiObject): + rel = "events" + type = "event" + version = "3.866.0" + template = { + "data": [ + { + "name": "type", + "value": "event" + }, + { + "name": "additional_location_details" + }, + { + "name": "browser_time_zone" + }, + { + "name": "division_location_id" + }, + { + "name": "doesnt_count_towards_record" + }, + { + "name": "duration_in_minutes" + }, + { + "name": "game_type_code" + }, + { + "name": "icon_color" + }, + { + "name": "is_canceled" + }, + { + "name": "is_game" + }, + { + "name": "is_overtime" + }, + { + "name": "is_shootout" + }, + { + "name": "is_tbd" + }, + { + "name": "label" + }, + { + "name": "location_id" + }, + { + "name": "minutes_to_arrive_early" + }, + { + "name": "name" + }, + { + "name": "notes" + }, + { + "name": "notify_opponent" + }, + { + "name": "notify_opponent_contacts_email" + }, + { + "name": "notify_opponent_contacts_name" + }, + { + "name": "notify_opponent_notes" + }, + { + "name": "notify_team" + }, + { + "name": "notify_team_as_member_id" + }, + { + "name": "opponent_id" + }, + { + "name": "points_for_opponent" + }, + { + "name": "points_for_team" + }, + { + "name": "repeating_include", + "prompt": "When updating a repeating event, this is a required field. Values are: \"all\" - updates all events in this series, \"future\" - updates this event and all that occur after, \"none\" - only updates a single event." + }, + { + "name": "repeating_type_code", + "prompt": "A app for the frequency of the repeated event, this is required with the \"repeating_include\" attribute when creating a repeating event. Valid values are: \"1\" - repeat an event daily, \"2\" - repeat an event weekly." + }, + { + "name": "repeating_until", + "prompt": "A date when the repeating event should end, this is inclusive so an event will be created on this day if it falls before the next event specified by \"repeating_type_code\". This attribute is required with \"repeating_type_code\" when creating a repeating event." + }, + { + "name": "results" + }, + { + "name": "results_url" + }, + { + "name": "shootout_points_for_opponent" + }, + { + "name": "shootout_points_for_team" + }, + { + "name": "start_date" + }, + { + "name": "team_id" + }, + { + "name": "time_zone" + }, + { + "name": "tracks_availability" + }, + { + "name": "uniform" + } + ]} + +class Team (ApiObject): + rel = "teams" + type = "team" + version = "3.866.0" + template = { + "data": [ + { + "name": "name" + }, + { + "name": "location_country" + }, + { + "name": "location_postal_code" + }, + { + "name": "time_zone", + "prompt": "The time_zone parameter is required when creating a team, but for changing a team's time_zone, use the update_time_zone command" + }, + { + "name": "sport_id" + }, + { + "name": "division_id" + }, + { + "name": "division_name" + }, + { + "name": "season_name" + }, + { + "name": "league_name" + }, + { + "name": "league_url" + }, + { + "name": "owner_first_name" + }, + { + "name": "owner_last_name" + }, + { + "name": "owner_email" + }, + { + "name": "is_ownership_pending" + }, + { + "name": "ad_unit_hero_id" + }, + { + "name": "ad_unit_hero_template_id" + }, + { + "name": "ad_unit_inline_id" + }, + { + "name": "type", + "value": "team" + } + ] + } + +class Availability (ApiObject): + rel = "availabilities" + type = "availability" + version = "3.866.0" + template = { + "data": [ + { + "name": "status_code" + }, + { + "name": "notes" + }, + { + "name": "event_id" + }, + { + "name": "member_id" + }, + { + "name": "notes_author_member_id" + }, + { + "name": "source" + }, + { + "name": "type", + "value": "availability" + } + ] + } + +class Member (ApiObject): + rel = "members" + type = "member" + version = "3.866.0" + template = { + "data": [ + { + "name": "first_name" + }, + { + "name": "last_name" + }, + { + "name": "gender" + }, + { + "name": "position" + }, + { + "name": "is_manager" + }, + { + "name": "birthday" + }, + { + "name": "hide_age", + "deprecated": True, + "prompt": "hide_age is deprecated and will be removed in a future version, use is_age_hidden instead." + }, + { + "name": "is_age_hidden" + }, + { + "name": "hide_address", + "deprecated": True, + "prompt": "hide_address is deprecated and will be removed in a future version, use is_address_hidden instead." + }, + { + "name": "is_address_hidden" + }, + { + "name": "is_non_player" + }, + { + "name": "address_street1" + }, + { + "name": "address_street2" + }, + { + "name": "address_city" + }, + { + "name": "address_state" + }, + { + "name": "address_zip" + }, + { + "name": "jersey_number" + }, + { + "name": "team_id" + }, + { + "name": "is_ownership_pending" + }, + { + "name": "source_action" + }, + { + "name": "type", + "value": "member" + } + ] + }, + +class Location (ApiObject): + rel = "locations" + type = "location" + version = "3.866.0" + template = { + "data": [ + { + "name": "name" + }, + { + "name": "url" + }, + { + "name": "phone" + }, + { + "name": "notes" + }, + { + "name": "address" + }, + { + "name": "latitude" + }, + { + "name": "longitude" + }, + { + "name": "team_id" + }, + { + "name": "is_retired" + }, + { + "name": "type", + "value": "location" + } + ] + } + +class Opponent (ApiObject): + rel = "opponents" + type = "opponent" + version = "3.866.0" + template = { + "data": [ + { + "name": "name" + }, + { + "name": "contacts_name" + }, + { + "name": "contacts_phone" + }, + { + "name": "contacts_email" + }, + { + "name": "notes" + }, + { + "name": "team_id" + }, + { + "name": "type", + "value": "opponent" + } + ] + } + +class EventLineupEntry (ApiObject): + rel = "event_lineup_entries" + type = "event_lineup_entry" + version = "3.866.0" + template = { + "data": [ + { + "name": "event_lineup_id" + }, + { + "name": "member_id" + }, + { + "name": "sequence" + }, + { + "name": "label" + }, + { + "name": "type", + "value": "event_lineup_entry" + } + ] + } + + @classmethod + def search(cls, client, **kwargs): + # For some reason the query listed for search at this endpoint is for EventLineup, not EventLineupEntry + # this is a workaround + r = client.get(f"{client.link(cls.rel)}/search", params=kwargs) + results = client.parse_response(r) + [cls(client, rel=cls.rel, data=r) for r in results] + return [cls(client, rel=cls.rel, data=r) for r in results] + +class EventLineup (ApiObject): + rel = "event_lineups" + type = "event_lineup" + version = "3.866.0" + template = {} + +class AvailabilitySummary (ApiObject): + rel = "availability_summaries" + type = "availability_summary" + version = "3.866.0" + template = {} + +class Statistics (ApiObject): + rel = "statistics" + type = "statistic" + version = "3.866.0" + template = { + "data": [ + { + "name": "acronym" + }, + { + "name": "always_display_decimals" + }, + { + "name": "formula" + }, + { + "name": "is_in_descending_order" + }, + { + "name": "display_zero_totals" + }, + { + "name": "is_percentage" + }, + { + "name": "is_private" + }, + { + "name": "is_team_statistic" + }, + { + "name": "is_top_statistic" + }, + { + "name": "name" + }, + { + "name": "precision" + }, + { + "name": "statistic_group_id" + }, + { + "name": "team_id" + }, + { + "name": "type", + "value": "statistic" + } + ] + } + +class MemberStatistics (ApiObject): + rel = "member_statistics" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3dfd3f2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +api-client +collection_json diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..0b055bc --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,12 @@ +pip==19.2.3 +bump2version==0.5.11 +wheel==0.33.6 +watchdog==0.9.0 +flake8==3.7.8 +tox==3.14.0 +coverage==4.5.4 +Sphinx==1.8.5 +twine==1.14.0 + + +black==21.7b0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a01d51e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[bumpversion] +current_version = 0.1.0 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:pyteamsnap/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + +[bdist_wheel] +universal = 1 + +[flake8] +exclude = docs diff --git a/setup.py b/setup.py index cdfdec9..29b26f7 100644 --- a/setup.py +++ b/setup.py @@ -1,24 +1,44 @@ +#!/usr/bin/env python + +"""The setup script.""" + from setuptools import setup, find_packages -VERSION = '0.0.1' -DESCRIPTION = 'Python package to interface with TeamSnap API' -LONG_DESCRIPTION = 'https://www.teamsnap.com/documentation' +with open('README.rst') as readme_file: + readme = readme_file.read() + +with open('HISTORY.rst') as history_file: + history = history_file.read() + +requirements = [ ] + +test_requirements = [ ] -# Setting up setup( - name="pyteamsnap", - version=VERSION, author="Anthony Correa", - author_email="a@correa.co", - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - packages=find_packages(), - install_requires=[ - "api-client", - "collection_json" - ], - - keywords=['teamsnap'], + author_email='a@correa.co', + python_requires='>=3.6', classifiers=[ - ] -) \ No newline at end of file + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ], + description="An unoffical wrapper for the TeamSnap API", + install_requires=requirements, + license="MIT license", + long_description=readme + '\n\n' + history, + include_package_data=True, + keywords='pyteamsnap', + name='pyteamsnap', + packages=find_packages(include=['pyteamsnap', 'pyteamsnap.*']), + test_suite='tests', + tests_require=test_requirements, + url='https://github.com/anthonyscorrea/pyteamsnap', + version='0.1.0', + zip_safe=False, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..aa04b53 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Unit test package for pyteamsnap.""" diff --git a/tests/test_pyteamsnap.py b/tests/test_pyteamsnap.py new file mode 100644 index 0000000..9229279 --- /dev/null +++ b/tests/test_pyteamsnap.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +"""Tests for `pyteamsnap` package.""" + + +import unittest + +from pyteamsnap import client +from os import getenv +TEAMSNAP_TOKEN = getenv('TEAMSNAP_TOKEN') +TEAMSNAP_TEAM = getenv('TEAMSNAP_TEAM') +TEAMSNAP_EVENT = getenv('TEAMSNAP_EVENT') + +class TestPyteamsnap(unittest.TestCase): + """Tests for `pyteamsnap` package.""" + + def setUp(self): + """Set up test fixtures, if any.""" + self.TEAMSNAP_TOKEN = getenv('TEAMSNAP_TOKEN') + self.TEAMSNAP_TEAM = getenv('TEAMSNAP_TEAM') + self.TEAMSNAP_EVENT = getenv('TEAMSNAP_EVENT') + + self.client = client.TeamSnap(token=TEAMSNAP_TOKEN) + + def tearDown(self): + """Tear down test fixtures, if any.""" + + def test_000_me(self): + """Test something.""" + from pyteamsnap.objects import Me + me = Me(self.client) + pass + + def test_001_bulk_load(self): + from pyteamsnap.objects import Event + events = self.client.bulk_load(team_id=self.TEAMSNAP_TEAM, types=[objects.Event]) + pass diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6baf232 --- /dev/null +++ b/tox.ini @@ -0,0 +1,19 @@ +[tox] +envlist = py36, py37, py38, flake8 + +[travis] +python = + 3.8: py38 + 3.7: py37 + 3.6: py36 + +[testenv:flake8] +basepython = python +deps = flake8 +commands = flake8 pyteamsnap tests + +[testenv] +setenv = + PYTHONPATH = {toxinidir} + +commands = python setup.py test