From e22ec9a271b5b263c7f58bdee7c9691ec8e07571 Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 14:08:52 +0900 Subject: [PATCH 1/7] Squashed 'vendor/asyncudp/' content from commit 62b859b0 git-subtree-dir: vendor/asyncudp git-subtree-split: 62b859b01260d2aa942bc58a7503809def747351 --- .github/FUNDING.yml | 1 + .github/workflows/pythonpackage.yml | 50 +++++ .gitignore | 72 +++++++ LICENSE | 22 +++ MANIFEST.in | 2 + README.rst | 56 ++++++ asyncudp/__init__.py | 125 ++++++++++++ asyncudp/version.py | 1 + docs/Makefile | 192 ++++++++++++++++++ docs/conf.py | 291 ++++++++++++++++++++++++++++ docs/index.rst | 55 ++++++ docs/make.bat | 263 +++++++++++++++++++++++++ examples/client.py | 12 ++ examples/server.py | 14 ++ setup.py | 29 +++ tests/__init__.py | 0 tests/test_asyncudp.py | 148 ++++++++++++++ 17 files changed, 1333 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/pythonpackage.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 asyncudp/__init__.py create mode 100644 asyncudp/version.py create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 examples/client.py create mode 100644 examples/server.py create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_asyncudp.py diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..91d9b70351 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: eerimoq diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000000..9b3f5cd31d --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,50 @@ +name: Test + +on: + push: + pull_request: + +jobs: + + linux: + + runs-on: ubuntu-20.04 + strategy: + max-parallel: 4 + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Test + run: | + python -m unittest + + release: + needs: [linux] + runs-on: ubuntu-20.04 + if: startsWith(github.ref, 'refs/tags') + + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: 3.9 + - name: Install pypa/build + run: | + python -m pip install build --user + - name: Build a source tarball + run: | + git clean -dfx + python -m build --sdist --outdir dist/ . + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + skip_existing: true + password: ${{secrets.pypi_password}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..d86d71d397 --- /dev/null +++ b/.gitignore @@ -0,0 +1,72 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so +*.o +a.out + +# Distribution / packaging +.Python +env/ +venv/ +.venv/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.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 + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Vim IDE +*~ +*.swp +*.swo + +# IntelliJ IDEA +.idea/ + +# pyenv +.python-version diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..2d5c757d4d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2021 Erik Moqvist + +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 0000000000..90b3902af1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include LICENSE +recursive-include tests *.py \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000..5b8ea965aa --- /dev/null +++ b/README.rst @@ -0,0 +1,56 @@ +Asyncio high level UDP sockets +============================== + +Asyncio high level UDP sockets. + +Project homepage: https://github.com/eerimoq/asyncudp + +Documentation: https://asyncudp.readthedocs.org/en/latest + +Installation +============ + +.. code-block:: python + + $ pip install asyncudp + +Example client +============== + +.. code-block:: python + + import asyncio + import asyncudp + + async def main(): + sock = await asyncudp.create_socket(remote_addr=('127.0.0.1', 9999)) + sock.sendto(b'Hello!') + print(await sock.recvfrom()) + sock.close() + + asyncio.run(main()) + +Example server +============== + +.. code-block:: python + + import asyncio + import asyncudp + + async def main(): + sock = await asyncudp.create_socket(local_addr=('127.0.0.1', 9999)) + + while True: + data, addr = await sock.recvfrom() + print(data, addr) + sock.sendto(data, addr) + + asyncio.run(main()) + +Test +==== + +.. code-block:: + + $ python3 -m unittest diff --git a/asyncudp/__init__.py b/asyncudp/__init__.py new file mode 100644 index 0000000000..2709e0ded9 --- /dev/null +++ b/asyncudp/__init__.py @@ -0,0 +1,125 @@ +import asyncio + +from .version import __version__ + + +class ClosedError(Exception): + pass + + +class _SocketProtocol: + + def __init__(self, packets_queue_max_size): + self._error = None + self._packets = asyncio.Queue(packets_queue_max_size) + + def connection_made(self, transport): + pass + + def connection_lost(self, transport): + self._packets.put_nowait(None) + + def datagram_received(self, data, addr): + self._packets.put_nowait((data, addr)) + + def error_received(self, exc): + self._error = exc + self._packets.put_nowait(None) + + async def recvfrom(self): + return await self._packets.get() + + def raise_if_error(self): + if self._error is None: + return + + error = self._error + self._error = None + + raise error + + +class Socket: + """A UDP socket. Use :func:`~asyncudp.create_socket()` to create an + instance of this class. + + """ + + def __init__(self, transport, protocol): + self._transport = transport + self._protocol = protocol + + def close(self): + """Close the socket. + + """ + + self._transport.close() + + def sendto(self, data, addr=None): + """Send given packet to given address ``addr``. Sends to + ``remote_addr`` given to the constructor if ``addr`` is + ``None``. + + Raises an error if a connection error has occurred. + + >>> sock.sendto(b'Hi!') + + """ + + self._transport.sendto(data, addr) + self._protocol.raise_if_error() + + async def recvfrom(self): + """Receive a UDP packet. + + Raises ClosedError on connection error, often by calling the + close() method from another task. May raise other errors as + well. + + >>> data, addr = sock.recvfrom() + + """ + + packet = await self._protocol.recvfrom() + self._protocol.raise_if_error() + + if packet is None: + raise ClosedError() + + return packet + + def getsockname(self): + """Get bound infomation. + + >>> local_address, local_port = sock.getsockname() + + """ + + return self._transport.get_extra_info('sockname') + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc_info): + self.close() + + +async def create_socket(local_addr=None, + remote_addr=None, + packets_queue_max_size=0, + reuse_port=None): + """Create a UDP socket with given local and remote addresses. + + >>> sock = await asyncudp.create_socket(local_addr=('127.0.0.1', 9999)) + + """ + + loop = asyncio.get_running_loop() + transport, protocol = await loop.create_datagram_endpoint( + lambda: _SocketProtocol(packets_queue_max_size), + local_addr=local_addr, + remote_addr=remote_addr, + reuse_port=reuse_port) + + return Socket(transport, protocol) diff --git a/asyncudp/version.py b/asyncudp/version.py new file mode 100644 index 0000000000..9d1bb721be --- /dev/null +++ b/asyncudp/version.py @@ -0,0 +1 @@ +__version__ = '0.10.0' diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..4fbb276295 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,192 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/asyncudp.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/asyncudp.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/asyncudp" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/asyncudp" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..d4733a2a41 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# +# asyncudp documentation build configuration file, created by +# sphinx-quickstart on Sat Apr 25 11:54:09 2015. +# +# 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. + +import sys +import os +import shlex + +# 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. +sys.path.insert(0, os.path.abspath('..')) + +import asyncudp + +# -- 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 encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'asyncudp' +copyright = u'2019, Erik Moqvist' +author = u'Erik Moqvist' + +# 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 = asyncudp.__version__ +# The full version, including alpha/beta/rc tags. +release = asyncudp.__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 + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# 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 = 'sphinx_rtd_theme' + +# 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 themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'asyncudpdoc' + +# -- 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, 'asyncudp.tex', u'asyncudp Documentation', + u'Erik Moqvist', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- 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, 'asyncudp', u'Asyncudp Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- 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, 'asyncudp', u'Asyncudp Documentation', + author, 'asyncudp', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +autodoc_member_order = 'bysource' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000..4f8c752f82 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,55 @@ +.. asyncudp documentation master file, created by + sphinx-quickstart on Sat Apr 25 11:54:09 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +.. include:: ../README.rst + +Examples +======== + +Client +------ + +.. code-block:: python + + import asyncio + import asyncudp + + async def main(): + sock = await asyncudp.create_socket(remote_addr=('127.0.0.1', 9999)) + sock.sendto(b'Hello!') + print(await sock.recvfrom()) + sock.close() + + asyncio.run(main()) + +Server +------ + +.. code-block:: python + + import asyncio + import asyncudp + + async def main(): + sock = await asyncudp.create_socket(local_addr=('127.0.0.1', 9999)) + + while True: + data, addr = await sock.recvfrom() + print(data, addr) + sock.sendto(data, addr) + + asyncio.run(main()) + +Functions and classes +===================== + +.. autofunction:: asyncudp.create_socket + +.. autoclass:: asyncudp.Socket + + .. automethod:: asyncudp.Socket.close + .. automethod:: asyncudp.Socket.sendto + .. automethod:: asyncudp.Socket.recvfrom + .. automethod:: asyncudp.Socket.getsockname diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..a54309f782 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\asyncudp.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\asyncudp.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/examples/client.py b/examples/client.py new file mode 100644 index 0000000000..471b95b79c --- /dev/null +++ b/examples/client.py @@ -0,0 +1,12 @@ +import asyncio +import asyncudp + + +async def main(): + sock = await asyncudp.create_socket(remote_addr=('127.0.0.1', 9999)) + sock.sendto(b'Hello!') + print(await sock.recvfrom()) + sock.close() + + +asyncio.run(main()) diff --git a/examples/server.py b/examples/server.py new file mode 100644 index 0000000000..b6b1534b95 --- /dev/null +++ b/examples/server.py @@ -0,0 +1,14 @@ +import asyncio +import asyncudp + + +async def main(): + sock = await asyncudp.create_socket(local_addr=('127.0.0.1', 9999)) + + while True: + data, addr = await sock.recvfrom() + print(data, addr) + sock.sendto(data, addr) + + +asyncio.run(main()) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..3e6fdf0a78 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import re +import setuptools +from setuptools import find_packages + + +def find_version(): + return re.search(r"^__version__ = '(.*)'$", + open('asyncudp/version.py', 'r').read(), + re.MULTILINE).group(1) + + +setuptools.setup( + name='asyncudp', + version=find_version(), + description='Asyncio high level UDP sockets.', + long_description=open('README.rst', 'r').read(), + author='Erik Moqvist', + author_email='erik.moqvist@gmail.com', + license='MIT', + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + ], + keywords=['asyncio'], + url='https://github.com/eerimoq/asyncudp', + packages=find_packages(exclude=['tests']), + test_suite="tests") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_asyncudp.py b/tests/test_asyncudp.py new file mode 100644 index 0000000000..d69f0d8547 --- /dev/null +++ b/tests/test_asyncudp.py @@ -0,0 +1,148 @@ +import asyncio +import unittest + +import asyncudp + + +class AsyncudpTest(unittest.TestCase): + + def test_local_addresses(self): + asyncio.run(self.local_addresses()) + + async def local_addresses(self): + server_addr = ('127.0.0.1', 13000) + client_addr = ('127.0.0.1', 13001) + + server = await asyncudp.create_socket(local_addr=server_addr) + client = await asyncudp.create_socket(local_addr=client_addr) + + client.sendto(b'local_addresses to server', server_addr) + data, addr = await server.recvfrom() + + self.assertEqual(data, b'local_addresses to server') + self.assertEqual(addr, client_addr) + + server.sendto(b'local_addresses to client', client_addr) + data, addr = await client.recvfrom() + + self.assertEqual(data, b'local_addresses to client') + self.assertEqual(addr, server_addr) + + server.close() + client.close() + + def test_getsockname(self): + asyncio.run(self.getsockname()) + + async def getsockname(self): + addr = ('127.0.0.1', 0) + socket = await asyncudp.create_socket(local_addr=addr) + actual_addr, actual_port = socket.getsockname() + self.assertTrue(actual_port > 0) + self.assertEqual(actual_addr, '127.0.0.1') + socket.close() + + def test_remote_address(self): + asyncio.run(self.remote_address()) + + async def remote_address(self): + server_addr = ('127.0.0.1', 13000) + client_addr = ('127.0.0.1', 13001) + + server = await asyncudp.create_socket(local_addr=server_addr) + client = await asyncudp.create_socket(local_addr=client_addr, + remote_addr=server_addr) + + client.sendto(b'remote_address to server') + data, addr = await server.recvfrom() + + self.assertEqual(data, b'remote_address to server') + self.assertEqual(addr, client_addr) + + server.close() + client.close() + + def test_cancel(self): + asyncio.run(self.cancel()) + + async def server_main(self, event): + server = await asyncudp.create_socket(local_addr=('127.0.0.1', 13000)) + + try: + await server.recvfrom() + except asyncio.CancelledError: + server.close() + event.set() + + async def cancel(self): + event = asyncio.Event() + task = asyncio.create_task(self.server_main(event)) + await asyncio.sleep(1.0) + task.cancel() + await event.wait() + + def test_context(self): + asyncio.run(self.context()) + + async def context(self): + server_addr = ('127.0.0.1', 13000) + client_addr = ('127.0.0.1', 13001) + + server = await asyncudp.create_socket(local_addr=server_addr) + client = await asyncudp.create_socket(local_addr=client_addr) + + async with server, client: + client.sendto(b'local_addresses to server', server_addr) + data, addr = await server.recvfrom() + + self.assertEqual(data, b'local_addresses to server') + self.assertEqual(addr, client_addr) + + server.sendto(b'local_addresses to client', client_addr) + data, addr = await client.recvfrom() + + self.assertEqual(data, b'local_addresses to client') + self.assertEqual(addr, server_addr) + + self.assertEqual(server._transport.is_closing(), False) + self.assertEqual(client._transport.is_closing(), False) + + self.assertEqual(server._transport.is_closing(), True) + self.assertEqual(client._transport.is_closing(), True) + + def test_packets_queue_max_size(self): + asyncio.run(self.packets_queue_max_size()) + + async def packets_queue_max_size(self): + server = await asyncudp.create_socket(local_addr=('127.0.0.1', 0), + packets_queue_max_size=1) + server_addr = server.getsockname() + client = await asyncudp.create_socket(remote_addr=server_addr) + + client.sendto(b'local_addresses to server 1') + client.sendto(b'local_addresses to server 2') + await asyncio.sleep(1.0) + data, _ = await server.recvfrom() + self.assertEqual(data, b'local_addresses to server 1') + + client.sendto(b'local_addresses to server 3') + data, _ = await server.recvfrom() + self.assertEqual(data, b'local_addresses to server 3') + + server.close() + client.close() + + def test_create_socket_reuse_port(self): + asyncio.run(self.create_socket_reuse_port()) + + async def create_socket_reuse_port(self): + sock = await asyncudp.create_socket(local_addr=('127.0.0.1', 13003), + reuse_port=True) + + with self.assertRaises(OSError): + await asyncudp.create_socket(local_addr=('127.0.0.1', 13003)) + + sock.close() + sock2 = await asyncudp.create_socket(local_addr=('127.0.0.1', 13003), + reuse_port=True) + sock2.close() From 3808f96b9aa0d9638e3ec1714eb2c70c825761d4 Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 14:39:30 +0900 Subject: [PATCH 2/7] setup: Use local requirements to refer the prebuilt vendored dependencies --- .gitattributes | 1 + pants.toml | 4 + python.lock | 301 +++++++++++++++++++++++++------------------------ 3 files changed, 159 insertions(+), 147 deletions(-) diff --git a/.gitattributes b/.gitattributes index 4b71a6869e..a249e06da7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ src/ai/backend/runner/*.ttf filter=lfs diff=lfs merge=lfs -text src/ai/backend/runner/*.bin filter=lfs diff=lfs merge=lfs -text src/ai/backend/runner/*.so filter=lfs diff=lfs merge=lfs -text src/ai/backend/runner/*.tar.xz filter=lfs diff=lfs merge=lfs -text +vendor/wheelhouse/*.whl filter=lfs diff=lfs merge=lfs -text diff --git a/pants.toml b/pants.toml index e026cd8974..cb0ff353d8 100644 --- a/pants.toml +++ b/pants.toml @@ -23,6 +23,8 @@ pants_ignore = [ "/docs/", # TODO: docs build config "*.log", "/tools/pants-plugins", + "/vendor/", + "!/vendor/wheelhouse", ] [anonymous-telemetry] @@ -57,6 +59,8 @@ search_path = [""] [python-repos] indexes = ["https://dist.backend.ai/pypi/simple/", "https://pypi.org/simple/"] +find_links = ["file://%(buildroot)s/vendor/wheelhouse"] +path_mappings = ["WHEELS_DIR|%(buildroot)s/vendor/wheelhouse"] [python.resolves] python-default = "python.lock" diff --git a/python.lock b/python.lock index a6be30b836..2004c39b8e 100644 --- a/python.lock +++ b/python.lock @@ -661,14 +661,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "856ac88ce1638ca2ba582cc23ae9c1a44f4b4d340a3188b2bd8286a72bf7f5cc", - "url": "https://files.pythonhosted.org/packages/15/ed/675e2abe68bdc299e67cc59bb73077bbe104a5eeedc6b367e249f122c40e/asyncudp-0.9.0.tar.gz" + "hash": "8e653538c1d78192d6117095cd90f82a7a56816a21eb3e22456ffcf798791c11", + "url": "file://${WHEELS_DIR}/asyncudp-0.10.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "b3655377e9637dfc2b4ba592f486bb17b2dddf25b2973e3c671785500f08b97a", + "url": "https://files.pythonhosted.org/packages/5c/ad/69ec91db8e295bdaeccfe6f90f5eb3ba1b48b73ac7746de217b4e5161ead/asyncudp-0.10.0.tar.gz" } ], "project_name": "asyncudp", "requires_dists": [], "requires_python": null, - "version": "0.9.0" + "version": "0.10.0" }, { "artifacts": [ @@ -858,36 +863,36 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "a5d6fdcaec863bc7ad2f8133ff9a926d6f06468b83b5fb631cd90bd33b709c45", - "url": "https://files.pythonhosted.org/packages/56/0c/44d0f6b2f29590c6e9a2f1239640b67a49cec00dfbaeb6352be47c29661d/boto3-1.26.141-py3-none-any.whl" + "hash": "fa85b67147c8dc99b6e7c699fc086103f958f9677db934f70659e6e6a72a818c", + "url": "https://files.pythonhosted.org/packages/62/fa/04e99eb81c703e0ef6c8e503fc7d74c8d836157ed15d61bc77f4fd1c6e73/boto3-1.26.165-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "152def2fcc9854dcc42383d2b53e2ed2c9ccb5ff6cc0f3ada20f1ab54418ede4", - "url": "https://files.pythonhosted.org/packages/06/70/9ffc65f8930b23f035f6797a5508eb0e991a638dd794161c8ca9077ac2d9/boto3-1.26.141.tar.gz" + "hash": "9e7242b9059d937f34264125fecd844cb5e01acce6be093f6c44869fdf7c6e30", + "url": "https://files.pythonhosted.org/packages/0f/3c/8a0b46a53326236006a4c4d1a0d49c4ff3a83368492c8308031fbaf61583/boto3-1.26.165.tar.gz" } ], "project_name": "boto3", "requires_dists": [ - "botocore<1.30.0,>=1.29.141", + "botocore<1.30.0,>=1.29.165", "botocore[crt]<2.0a0,>=1.21.0; extra == \"crt\"", "jmespath<2.0.0,>=0.7.1", "s3transfer<0.7.0,>=0.6.0" ], "requires_python": ">=3.7", - "version": "1.26.141" + "version": "1.26.165" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "b01d156c42765f3f437959e01a8c7f3cb0e29b24aa0b8f373498133408b2e3c7", - "url": "https://files.pythonhosted.org/packages/ce/15/989f8da9ba16530958836c426f1e233ec69f95a82ca5a3f5a614a1a3e374/botocore-1.29.141-py3-none-any.whl" + "hash": "6f35d59e230095aed7cd747604fe248fa384bebb7d09549077892f936a8ca3df", + "url": "https://files.pythonhosted.org/packages/46/20/e7a9a8e6746872afcc4e3ad5ab503702c38813b3a532df27cce95c98b8cb/botocore-1.29.165-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "e86e1633f98838317b9e1b5c874c4d85339b77f6b7e55c2a4d83913f6166f9ad", - "url": "https://files.pythonhosted.org/packages/dd/c9/92fdc5d97a9936cd72a95148bc99cf050a492a6a110482953d4dd1f0c985/botocore-1.29.141.tar.gz" + "hash": "988b948be685006b43c4bbd8f5c0cb93e77c66deb70561994e0c5b31b5a67210", + "url": "https://files.pythonhosted.org/packages/3d/f6/d35a27c73dc1053abdfe8524d1e488073fccb51e43c88da61b8fe29522e3/botocore-1.29.165.tar.gz" } ], "project_name": "botocore", @@ -898,7 +903,7 @@ "urllib3<1.27,>=1.25.4" ], "requires_python": ">=3.7", - "version": "1.29.141" + "version": "1.29.165" }, { "artifacts": [ @@ -1225,89 +1230,86 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c", - "url": "https://files.pythonhosted.org/packages/91/89/13174c6167f452598baa8584133993e3d624b6a19e93748e5f2885a442f2/cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl" + "hash": "b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31", + "url": "https://files.pythonhosted.org/packages/52/4c/a5b0cabca7033510d490b5a9fddce62f87a0420ddc4d96b1ab4435f10f75/cryptography-41.0.1-cp37-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d", - "url": "https://files.pythonhosted.org/packages/0d/91/b2efda2ffb30b1623016d8e8ea6f59dde22b9bc86c0883bc12d965c53dca/cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca", + "url": "https://files.pythonhosted.org/packages/12/82/8d41bda1fc6e5a51ae4f47abc910e40c0207233bf44f2bcd794272db2c69/cryptography-41.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440", - "url": "https://files.pythonhosted.org/packages/85/86/a17a4baf08e0ae6496b44f75136f8e14b843fd3d8a3f4105c0fd79d4786b/cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl" + "hash": "d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006", + "url": "https://files.pythonhosted.org/packages/19/8c/47f061de65d1571210dc46436c14a0a4c260fd0f3eaf61ce9b9d445ce12f/cryptography-41.0.1.tar.gz" }, { "algorithm": "sha256", - "hash": "d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9", - "url": "https://files.pythonhosted.org/packages/88/87/c720c0b56f6363eaa32c582b6240523010691ad973204649526c4ce28e95/cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl" + "hash": "b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43", + "url": "https://files.pythonhosted.org/packages/32/86/2037a52402f8d03f7a2be172ffb4bbac0250c54e51d50136c0c6c4e0cf70/cryptography-41.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b", - "url": "https://files.pythonhosted.org/packages/8e/34/f54dbfc6d12fa34a50f03bf01319d585e7e9bddd68ad28299b4998e3098b/cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl" + "hash": "948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3", + "url": "https://files.pythonhosted.org/packages/49/35/80c346e1a9509210defa857a05e9b7931093719aab25665d4d54f9b3ba83/cryptography-41.0.1-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288", - "url": "https://files.pythonhosted.org/packages/9c/1b/30faebcef9be2df5728a8086b8fc15fff92364fe114fb207b70cd7c81329/cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b", + "url": "https://files.pythonhosted.org/packages/b7/88/3e6c5eda9ab474fa9b0cf84e6119385aaefbe5c9700a5eacd6e0a9f415bb/cryptography-41.0.1-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b", - "url": "https://files.pythonhosted.org/packages/cc/aa/285f288e36d398db873d4cc20984c9a132ef5eace539d91babe4c4e94aaa/cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl" + "hash": "f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699", + "url": "https://files.pythonhosted.org/packages/d8/80/e32f30266381f6ca05ee4aa92ce5f305aa1acbef4117a9a8d94d9b60bb67/cryptography-41.0.1-cp37-abi3-macosx_10_12_universal2.whl" }, { "algorithm": "sha256", - "hash": "c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99", - "url": "https://files.pythonhosted.org/packages/f7/80/04cc7637238b78f8e7354900817135c5a23cf66dfb3f3a216c6d630d6833/cryptography-40.0.2.tar.gz" + "hash": "1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a", + "url": "https://files.pythonhosted.org/packages/eb/09/6b2c7f6dcf756f318cc232576c2198c114758510317ddade9490e568362a/cryptography-41.0.1-cp37-abi3-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2", - "url": "https://files.pythonhosted.org/packages/ff/87/cffd495cc78503fb49aa3e19babc126b610174d08aa32c0d1d75c6499afc/cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl" + "hash": "059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db", + "url": "https://files.pythonhosted.org/packages/ef/78/d391ec7a08d4adf8a93d0fd9fa9fd468493ef50b6213c28deadf5322379d/cryptography-41.0.1-cp37-abi3-musllinux_1_1_aarch64.whl" } ], "project_name": "cryptography", "requires_dists": [ "bcrypt>=3.1.5; extra == \"ssh\"", "black; extra == \"pep8test\"", + "build; extra == \"sdist\"", "cffi>=1.12", - "check-manifest; extra == \"pep8test\"", - "iso8601; extra == \"test\"", + "check-sdist; extra == \"pep8test\"", "mypy; extra == \"pep8test\"", + "nox; extra == \"nox\"", "pretend; extra == \"test\"", "pyenchant>=1.6.11; extra == \"docstest\"", "pytest-benchmark; extra == \"test\"", "pytest-cov; extra == \"test\"", "pytest-randomly; extra == \"test-randomorder\"", - "pytest-shard>=0.1.2; extra == \"test\"", - "pytest-subtests; extra == \"test\"", "pytest-xdist; extra == \"test\"", "pytest>=6.2.0; extra == \"test\"", "ruff; extra == \"pep8test\"", - "setuptools-rust>=0.11.4; extra == \"sdist\"", "sphinx-rtd-theme>=1.1.1; extra == \"docs\"", "sphinx>=5.3.0; extra == \"docs\"", "sphinxcontrib-spelling>=4.0.1; extra == \"docstest\"", - "tox; extra == \"tox\"", "twine>=1.12.0; extra == \"docstest\"" ], - "requires_python": ">=3.6", - "version": "40.0.2" + "requires_python": ">=3.7", + "version": "41.0.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "bc285b5f892094c3a53d558858a88553dd6a61a11ab1a8128a0e554385dcc5dd", - "url": "https://files.pythonhosted.org/packages/58/7e/2042610dfc8121e8119ad8b94db496d8697e4b0ef7a6e378018a2bd84435/dataclasses_json-0.5.7-py3-none-any.whl" + "hash": "1280542631df1c375b7bc92e5b86d39e06c44760d7e3571a537b3b8acabf2f0c", + "url": "https://files.pythonhosted.org/packages/eb/04/2851f9fe4b01b5b752c16e41d581f6b9d0ca82e388d7bd58357d758fc6ce/dataclasses_json-0.5.9-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "c2c11bc8214fbf709ffc369d11446ff6945254a7f09128154a7620613d8fda90", - "url": "https://files.pythonhosted.org/packages/85/94/1b30216f84c48b9e0646833f6f2dd75f1169cc04dc45c48fe39e644c89d5/dataclasses-json-0.5.7.tar.gz" + "hash": "e9ac87b73edc0141aafbce02b44e93553c3123ad574958f0fe52a534b6707e8e", + "url": "https://files.pythonhosted.org/packages/86/c2/db1972ba8fda56d1c3317d8cca1c06af4b5df7e3c94345048d10cf4c7bf4/dataclasses-json-0.5.9.tar.gz" } ], "project_name": "dataclasses-json", @@ -1320,13 +1322,16 @@ "marshmallow<4.0.0,>=3.3.0", "mypy>=0.710; extra == \"dev\"", "portray; extra == \"dev\"", - "pytest>=6.2.3; extra == \"dev\"", + "pytest>=7.2.0; extra == \"dev\"", + "setuptools; extra == \"dev\"", "simplejson; extra == \"dev\"", + "twine; extra == \"dev\"", "types-dataclasses; python_version == \"3.6\" and extra == \"dev\"", - "typing-inspect>=0.4.0" + "typing-inspect>=0.4.0", + "wheel; extra == \"dev\"" ], "requires_python": ">=3.6", - "version": "0.5.7" + "version": "0.5.9" }, { "artifacts": [ @@ -1466,18 +1471,18 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "be617bfaf77774008e9d177573f782e109188c8a64ae6e744285df5cea3e7df6", - "url": "https://files.pythonhosted.org/packages/d1/66/ae0916fa2e741d28906ebf8cde3a6b48e4cb2558f4effdf30cec067bbc09/google_auth-2.19.0-py2.py3-none-any.whl" + "hash": "da3f18d074fa0f5a7061d99b9af8cee3aa6189c987af7c1b07d94566b6b11268", + "url": "https://files.pythonhosted.org/packages/0d/77/4737ca3b929e95df9234827f7ddcf66199df2d96057ba9a98168957de7fa/google_auth-2.21.0-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "f39d528077ac540793dd3c22a8706178f157642a67d874db25c640b7fead277e", - "url": "https://files.pythonhosted.org/packages/97/e8/a3e918ab46e91e87019e2f96aec2390e11177da536b7bc75d14c8534d9c9/google-auth-2.19.0.tar.gz" + "hash": "b28e8048e57727e7cf0e5bd8e7276b212aef476654a09511354aa82753b45c66", + "url": "https://files.pythonhosted.org/packages/e6/76/2217d47be6a03dfd5f6bfe7432f87a5aa5ab6d85525a4dc546df9895f053/google-auth-2.21.0.tar.gz" } ], "project_name": "google-auth", "requires_dists": [ - "aiohttp<4.0.0dev,>=3.6.2; extra == \"aiohttp\"", + "aiohttp<4.0.0.dev0,>=3.6.2; extra == \"aiohttp\"", "cachetools<6.0,>=2.0.0", "cryptography==36.0.2; extra == \"enterprise_cert\"", "cryptography>=38.0.3; extra == \"pyopenssl\"", @@ -1485,14 +1490,14 @@ "pyopenssl==22.0.0; extra == \"enterprise_cert\"", "pyopenssl>=20.0.0; extra == \"pyopenssl\"", "pyu2f>=0.1.5; extra == \"reauth\"", - "requests<3.0.0dev,>=2.20.0; extra == \"aiohttp\"", - "requests<3.0.0dev,>=2.20.0; extra == \"requests\"", + "requests<3.0.0.dev0,>=2.20.0; extra == \"aiohttp\"", + "requests<3.0.0.dev0,>=2.20.0; extra == \"requests\"", "rsa<5,>=3.1.4", "six>=1.9.0", "urllib3<2.0" ], "requires_python": ">=3.6", - "version": "2.19.0" + "version": "2.21.0" }, { "artifacts": [ @@ -1853,24 +1858,23 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "401201aca462749773f02920139f302450cb548b70489b9b4b92be39fe3c3c50", - "url": "https://files.pythonhosted.org/packages/22/2b/30e8725481b071ca53984742a443f944f9c74fb72f509a40b746912645e1/humanize-4.6.0-py3-none-any.whl" + "hash": "df7c429c2d27372b249d3f26eb53b07b166b661326e0325793e0a988082e3889", + "url": "https://files.pythonhosted.org/packages/5e/81/60bbbb745b397fa56b82ec71ecbada00f574319b8f36c5f53c6c0c0c0601/humanize-4.7.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "5f1f22bc65911eb1a6ffe7659bd6598e33dcfeeb904eb16ee1e705a09bf75916", - "url": "https://files.pythonhosted.org/packages/06/b1/9e491df2ee1c919d67ee328d8bc9f17b7a9af68e4077f3f5fac83a4488c9/humanize-4.6.0.tar.gz" + "hash": "7ca0e43e870981fa684acb5b062deb307218193bca1a01f2b2676479df849b3a", + "url": "https://files.pythonhosted.org/packages/69/86/34d04afc5c33a31f4e9939f857e28fc9d039440f29b99a34f2190f0ab0ac/humanize-4.7.0.tar.gz" } ], "project_name": "humanize", "requires_dists": [ "freezegun; extra == \"tests\"", - "importlib-metadata; python_version < \"3.8\"", "pytest-cov; extra == \"tests\"", "pytest; extra == \"tests\"" ], - "requires_python": ">=3.7", - "version": "4.6.0" + "requires_python": ">=3.8", + "version": "4.7.0" }, { "artifacts": [ @@ -2011,13 +2015,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "b18219aa695d39e2ad570533e0d71fb7881d35a873051054a84ee2a17c4b7389", - "url": "https://files.pythonhosted.org/packages/07/37/4019d2c41ca333c08dfdfeb84c0fc0368c8defbbd3c8f0c9a530851e5813/jupyter_client-8.2.0-py3-none-any.whl" + "hash": "7441af0c0672edc5d28035e92ba5e32fadcfa8a4e608a434c228836a89df6158", + "url": "https://files.pythonhosted.org/packages/29/24/0491f7837cedf39ae0f96d9b3e4db2fae31cc4dd5eac00a98ab0db996c9b/jupyter_client-8.3.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9fe233834edd0e6c0aa5f05ca2ab4bdea1842bfd2d8a932878212fc5301ddaf0", - "url": "https://files.pythonhosted.org/packages/95/81/e9d897aa0f8ae679da86fab982900f5e40c37ebd81b04a3e88f26a201517/jupyter_client-8.2.0.tar.gz" + "hash": "3af69921fe99617be1670399a0b857ad67275eefcfa291e2c81a160b7b650f5f", + "url": "https://files.pythonhosted.org/packages/f9/bb/454464291217af5dc1d0dfc636f7f6b68227758319dad5f64b341ffd54f5/jupyter_client-8.3.0.tar.gz" } ], "project_name": "jupyter-client", @@ -2046,19 +2050,19 @@ "traitlets>=5.3" ], "requires_python": ">=3.8", - "version": "8.2.0" + "version": "8.3.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "d4201af84559bc8c70cead287e1ab94aeef3c512848dde077b7684b54d67730d", - "url": "https://files.pythonhosted.org/packages/41/1e/92a67f333b9335f04ce409799c030dcfb291712658b9d9d13997f7c91e5a/jupyter_core-5.3.0-py3-none-any.whl" + "hash": "ae9036db959a71ec1cac33081eeb040a79e681f08ab68b0883e9a676c7a90dce", + "url": "https://files.pythonhosted.org/packages/8c/e0/3f9061c5e99a03612510f892647b15a91f910c5275b7b77c6c72edae1494/jupyter_core-5.3.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "6db75be0c83edbf1b7c9f91ec266a9a24ef945da630f3120e1a0046dc13713fc", - "url": "https://files.pythonhosted.org/packages/9a/d3/b80e7179e9615f5a7f055edc55eb665fa2534f11a4599349db3bab6fdeb5/jupyter_core-5.3.0.tar.gz" + "hash": "5ba5c7938a7f97a6b0481463f7ff0dbac7c15ba48cf46fa4035ca6e838aa1aba", + "url": "https://files.pythonhosted.org/packages/9e/53/f27bd74ceaa672a1ce17b4b2bee93c0742ca00cb9f540ec4fa60cf7319b5/jupyter_core-5.3.1.tar.gz" } ], "project_name": "jupyter-core", @@ -2078,7 +2082,7 @@ "traitlets>=5.3" ], "requires_python": ">=3.8", - "version": "5.3.0" + "version": "5.3.1" }, { "artifacts": [ @@ -2177,54 +2181,54 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "url": "https://files.pythonhosted.org/packages/1f/20/76f6337f1e7238a626ab34405ddd634636011b2ff947dcbd8995f16a7776/MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "url": "https://files.pythonhosted.org/packages/bb/82/f88ccb3ca6204a4536cf7af5abdad7c3657adac06ab33699aa67279e0744/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "url": "https://files.pythonhosted.org/packages/04/cf/9464c3c41b7cdb8df660cda75676697e7fb49ce1be7691a1162fc88da078/MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl" + "hash": "df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "url": "https://files.pythonhosted.org/packages/32/d4/ce98c4ca713d91c4a17c1a184785cc00b9e9c25699d618956c2b9999500a/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl" }, { "algorithm": "sha256", - "hash": "65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "url": "https://files.pythonhosted.org/packages/0a/88/78cb3d95afebd183d8b04442685ab4c70cfc1138b850ba20e2a07aff2f53/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "url": "https://files.pythonhosted.org/packages/43/70/f24470f33b2035b035ef0c0ffebf57006beb2272cf3df068fc5154e04ead/MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "url": "https://files.pythonhosted.org/packages/5a/94/d056bf5dbadf7f4b193ee2a132b3d49ffa1602371e3847518b2982045425/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "url": "https://files.pythonhosted.org/packages/6d/7c/59a3248f411813f8ccba92a55feaac4bf360d29e2ff05ee7d8e1ef2d7dbf/MarkupSafe-2.1.3.tar.gz" }, { "algorithm": "sha256", - "hash": "da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "url": "https://files.pythonhosted.org/packages/79/e2/b818bf277fa6b01244943498cb2127372c01dde5eff7682837cc72740618/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + "hash": "b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "url": "https://files.pythonhosted.org/packages/a2/f7/9175ad1b8152092f7c3b78c513c1bdfe9287e0564447d1c2d3d1a2471540/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "url": "https://files.pythonhosted.org/packages/95/7e/68018b70268fb4a2a605e2be44ab7b4dd7ce7808adae6c5ef32e34f4b55a/MarkupSafe-2.1.2.tar.gz" + "hash": "3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "url": "https://files.pythonhosted.org/packages/c0/c7/171f5ac6b065e1425e8fabf4a4dfbeca76fd8070072c6a41bd5c07d90d8b/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "url": "https://files.pythonhosted.org/packages/cf/c1/d7596976a868fe3487212a382cc121358a53dc8e8d85ff2ee2c3d3b40f04/MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl" + "hash": "338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "url": "https://files.pythonhosted.org/packages/f4/a0/103f94793c3bf829a18d2415117334ece115aeca56f2df1c47fa02c6dbd6/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" }, { "algorithm": "sha256", - "hash": "2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "url": "https://files.pythonhosted.org/packages/e3/a9/e366665c7eae59c9c9d34b747cd5a3994847719a2304e0c8dec8b604dd98/MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl" + "hash": "ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "url": "https://files.pythonhosted.org/packages/fe/09/c31503cb8150cf688c1534a7135cc39bb9092f8e0e6369ec73494d16ee0e/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl" }, { "algorithm": "sha256", - "hash": "608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "url": "https://files.pythonhosted.org/packages/e6/ff/d2378ca3cb3ac4a37af767b820b0f0bf3f5e9193a6acce0eefc379425c1c/MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl" + "hash": "bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "url": "https://files.pythonhosted.org/packages/fe/21/2eff1de472ca6c99ec3993eab11308787b9879af9ca8bbceb4868cf4f2ca/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } ], "project_name": "markupsafe", "requires_dists": [], "requires_python": ">=3.7", - "version": "2.1.2" + "version": "2.1.3" }, { "artifacts": [ @@ -2605,42 +2609,42 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5", - "url": "https://files.pythonhosted.org/packages/89/7e/c6ff9ddcf93b9b36c90d88111c4db354afab7f9a58c7ac3257fa717f1268/platformdirs-3.5.1-py3-none-any.whl" + "hash": "ca9ed98ce73076ba72e092b23d3c93ea6c4e186b3f1c3dad6edd98ff6ffcca2e", + "url": "https://files.pythonhosted.org/packages/e7/61/7fde5beff25a0dae6c2056203696169bd29188b6cedefff8ba6e7b54417b/platformdirs-3.8.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f", - "url": "https://files.pythonhosted.org/packages/9c/0e/ae9ef1049d4b5697e79250c4b2e72796e4152228e67733389868229c92bb/platformdirs-3.5.1.tar.gz" + "hash": "b0cabcb11063d21a0b261d557acb0a9d2126350e63b70cdf7db6347baea456dc", + "url": "https://files.pythonhosted.org/packages/cb/10/e5478cc0c3ee5563f91ab7b9da15d16e21f3737b6286ed3fd9a8fb1a99dd/platformdirs-3.8.0.tar.gz" } ], "project_name": "platformdirs", "requires_dists": [ "appdirs==1.4.4; extra == \"test\"", "covdefaults>=2.3; extra == \"test\"", - "furo>=2023.3.27; extra == \"docs\"", + "furo>=2023.5.20; extra == \"docs\"", "proselint>=0.13; extra == \"docs\"", - "pytest-cov>=4; extra == \"test\"", + "pytest-cov>=4.1; extra == \"test\"", "pytest-mock>=3.10; extra == \"test\"", "pytest>=7.3.1; extra == \"test\"", "sphinx-autodoc-typehints!=1.23.4,>=1.23; extra == \"docs\"", - "sphinx>=6.2.1; extra == \"docs\"", - "typing-extensions>=4.5; python_version < \"3.8\"" + "sphinx>=7.0.1; extra == \"docs\"", + "typing-extensions>=4.6.3; python_version < \"3.8\"" ], "requires_python": ">=3.7", - "version": "3.5.1" + "version": "3.8.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3", - "url": "https://files.pythonhosted.org/packages/9e/01/f38e2ff29715251cf25532b9082a1589ab7e4f571ced434f98d0139336dc/pluggy-1.0.0-py2.py3-none-any.whl" + "hash": "c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", + "url": "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "url": "https://files.pythonhosted.org/packages/a1/16/db2d7de3474b6e37cbb9c008965ee63835bba517e22cdb8c35b5116b5ce1/pluggy-1.0.0.tar.gz" + "hash": "d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", + "url": "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz" } ], "project_name": "pluggy", @@ -2651,8 +2655,8 @@ "pytest; extra == \"testing\"", "tox; extra == \"dev\"" ], - "requires_python": ">=3.6", - "version": "1.0.0" + "requires_python": ">=3.7", + "version": "1.2.0" }, { "artifacts": [ @@ -3032,13 +3036,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", - "url": "https://files.pythonhosted.org/packages/1b/d1/72df649a705af1e3a09ffe14b0c7d3be1fd730da6b98beb4a2ed26b8a023/pytest-7.3.1-py3-none-any.whl" + "hash": "78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", + "url": "https://files.pythonhosted.org/packages/33/b2/741130cbcf2bbfa852ed95a60dc311c9e232c7ed25bac3d9b8880a8df4ae/pytest-7.4.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3", - "url": "https://files.pythonhosted.org/packages/ec/d9/36b65598f3d19d0a14d13dc87ad5fa42869ae53bb7471f619a30eaabc4bf/pytest-7.3.1.tar.gz" + "hash": "b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a", + "url": "https://files.pythonhosted.org/packages/a7/f3/dadfbdbf6b6c8b5bd02adb1e08bc9fbb45ba51c68b0893fa536378cdf485/pytest-7.4.0.tar.gz" } ], "project_name": "pytest", @@ -3056,11 +3060,12 @@ "pluggy<2.0,>=0.12", "pygments>=2.7.2; extra == \"testing\"", "requests; extra == \"testing\"", + "setuptools; extra == \"testing\"", "tomli>=1.0.0; python_version < \"3.11\"", "xmlschema; extra == \"testing\"" ], "requires_python": ">=3.7", - "version": "7.3.1" + "version": "7.4.0" }, { "artifacts": [ @@ -3487,13 +3492,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f", - "url": "https://files.pythonhosted.org/packages/f5/2c/074ab1c5be9c7d523d8d6d69d1f46f450fe7f11713147dc9e779aa4ca4ea/setuptools-67.8.0-py3-none-any.whl" + "hash": "11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", + "url": "https://files.pythonhosted.org/packages/c7/42/be1c7bbdd83e1bfb160c94b9cafd8e25efc7400346cf7ccdbdb452c467fa/setuptools-68.0.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102", - "url": "https://files.pythonhosted.org/packages/03/20/630783571e76e5fa5f3e9f29398ca3ace377207b8196b54e0ffdf09f12c1/setuptools-67.8.0.tar.gz" + "hash": "baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235", + "url": "https://files.pythonhosted.org/packages/dc/98/5f896af066c128669229ff1aa81553ac14cfb3e5e74b6b44594132b8540e/setuptools-68.0.0.tar.gz" } ], "project_name": "setuptools", @@ -3544,7 +3549,7 @@ "wheel; extra == \"testing-integration\"" ], "requires_python": ">=3.7", - "version": "67.8.0" + "version": "68.0.0" }, { "artifacts": [ @@ -3904,19 +3909,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "9634f06c5fc331ec660d1e80faa031aa2e0aa388fbb6312f936c7076ac8a9fd3", - "url": "https://files.pythonhosted.org/packages/59/f8/db22544537e2ddc4ec6c6429aa59726a0e26d8cd47236fb1c3fea2e7fec0/types_aiofiles-23.1.0.3-py3-none-any.whl" + "hash": "65a862f0d36e6b8e1b2df601ba7aeeb2eba8e2f9764ba9d0989bdd498ed8c857", + "url": "https://files.pythonhosted.org/packages/49/8b/096aea5dc91d995dfbbd93f8f551c231ac55cafa8cdf6e25ec84f4f665bd/types_aiofiles-23.1.0.4-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "ccd7b4db06c66cb7145802b1d9cf33bd6d8e88c7fe63a3ba5551671faac32648", - "url": "https://files.pythonhosted.org/packages/8f/e9/8259e6cbb9b80180f2034d3ae46c1ec0cd66d331ddb2d4e6c8280c36f3f5/types-aiofiles-23.1.0.3.tar.gz" + "hash": "89a58cd0ae93b37a22c323c22d250bd65dde6833f6c6f3c1824784e56f47109f", + "url": "https://files.pythonhosted.org/packages/89/f8/9e5384d2bdebf5960565a2b2c7ceccdf7a63d1161e87acde4364453f1410/types-aiofiles-23.1.0.4.tar.gz" } ], "project_name": "types-aiofiles", "requires_dists": [], "requires_python": null, - "version": "23.1.0.3" + "version": "23.1.0.4" }, { "artifacts": [ @@ -3996,13 +4001,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "ad024b07a1f4bffbca44699543c71efd04733a6c22781fa9673a971e410a3086", - "url": "https://files.pythonhosted.org/packages/88/71/c0d92ea9c7c21b941306f97a3fcd85a6b57ff57f84d9ba16083cb2286664/types_pyOpenSSL-23.1.0.3-py3-none-any.whl" + "hash": "0568553f104466f1b8e0db3360fbe6770137d02e21a1a45c209bf2b1b03d90d4", + "url": "https://files.pythonhosted.org/packages/41/b9/fec82efcbc552586ac4705d6d0c9cb7bff0eb5494316337f76a429ac89ea/types_pyOpenSSL-23.2.0.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "e7211088eff3e20d359888dedecb0994f7181d5cce0f26354dd47ca0484dc8a6", - "url": "https://files.pythonhosted.org/packages/a5/f0/7c331aa7a58d421b41aaf69ca72a1633a7f3e724a8e3646a0b10357af16f/types-pyOpenSSL-23.1.0.3.tar.gz" + "hash": "beeb5d22704c625a1e4b6dc756355c5b4af0b980138b702a9d9f932acf020903", + "url": "https://files.pythonhosted.org/packages/ab/2f/c2832fc4242240120e5e07bf071ec624453089191b8f78b69740e5f35873/types-pyOpenSSL-23.2.0.1.tar.gz" } ], "project_name": "types-pyopenssl", @@ -4010,7 +4015,7 @@ "cryptography>=35.0.0" ], "requires_python": null, - "version": "23.1.0.3" + "version": "23.2.0.1" }, { "artifacts": [ @@ -4052,13 +4057,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "bf8692252038dbe03b007ca4fde87d3ae8e10610854a6858e3bf5d01721a7c4b", - "url": "https://files.pythonhosted.org/packages/6b/5d/f3c257c289d47ba9b988712e35308062f8fbbe2af45121c52c53f806ff00/types_redis-4.5.5.2-py3-none-any.whl" + "hash": "88ceb79c27f2084ad6f0b8514f8fcd8a740811f07c25f3fef5c9e843fc6c60a2", + "url": "https://files.pythonhosted.org/packages/7b/c9/ce3c4c299f2f78d01bebf2e5badf08ce42f496f9220b857c59c24b503754/types_redis-4.6.0.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "2fe82f374d9dddf007deaf23d81fddcfd9523d9522bf11523c5c43bc5b27099e", - "url": "https://files.pythonhosted.org/packages/ef/77/25f255db176b5a49028be5b3577506002f831d5d2a45c5cd9ced5beb116a/types-redis-4.5.5.2.tar.gz" + "hash": "1254d525de7a45e2efaacb6969e67ad1dd5cc359a092022200583a3f04868669", + "url": "https://files.pythonhosted.org/packages/b0/5b/3d514eb5708923dfa5b5a02c2e9f14a41f984b963667c0949978845a50dd/types-redis-4.6.0.1.tar.gz" } ], "project_name": "types-redis", @@ -4067,25 +4072,25 @@ "types-pyOpenSSL" ], "requires_python": null, - "version": "4.5.5.2" + "version": "4.6.0.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff", - "url": "https://files.pythonhosted.org/packages/b0/17/3b4d69a9339b8aeac751a3f62a0875b345518f69db8d5b3f6272f7c10ff1/types_setuptools-67.8.0.0-py3-none-any.whl" + "hash": "cc00e09ba8f535362cbe1ea8b8407d15d14b59c57f4190cceaf61a9e57616446", + "url": "https://files.pythonhosted.org/packages/67/6b/cc6fdd6a233a0075dafacd797a40b5808cffc5254b48a179f9f8204c815e/types_setuptools-68.0.0.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f", - "url": "https://files.pythonhosted.org/packages/9f/b1/01280c629276c7fec2580ba90b48955397a178d899936567a9ab98ba5afe/types-setuptools-67.8.0.0.tar.gz" + "hash": "fc958b4123b155ffc069a66d3af5fe6c1f9d0600c35c0c8444b2ab4147112641", + "url": "https://files.pythonhosted.org/packages/02/3a/a2cc23768db03109c2005d425261ce3665e6f084c35de7b681e78542d1b6/types-setuptools-68.0.0.0.tar.gz" } ], "project_name": "types-setuptools", "requires_dists": [], "requires_python": null, - "version": "67.8.0.0" + "version": "68.0.0.0" }, { "artifacts": [ @@ -4127,19 +4132,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98", - "url": "https://files.pythonhosted.org/packages/38/60/300ad6f93adca578bf05d5f6cd1d854b7d140bebe2f9829561aa9977d9f3/typing_extensions-4.6.2-py3-none-any.whl" + "hash": "440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", + "url": "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c", - "url": "https://files.pythonhosted.org/packages/be/fc/3d12393d634fcb31d5f4231c28feaf4ead225124ba08021046317d5f450d/typing_extensions-4.6.2.tar.gz" + "hash": "b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", + "url": "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz" } ], "project_name": "typing-extensions", "requires_dists": [], "requires_python": ">=3.7", - "version": "4.6.2" + "version": "4.7.1" }, { "artifacts": [ @@ -4281,13 +4286,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "f8c64e28cd700e7ba1f04350d66422b6833b82a796b525a51e740b8cc8dab4b1", - "url": "https://files.pythonhosted.org/packages/86/5c/2ebfbb7d4dbb7f35a1f70c40d003f7844d78945ac7c69757067ebaea9c78/websocket_client-1.5.2-py3-none-any.whl" + "hash": "f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d", + "url": "https://files.pythonhosted.org/packages/d3/a3/63e9329c8cc9be6153e919e17d0ef5b60d537fed78564872951b95bcc17c/websocket_client-1.6.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "c7d67c13b928645f259d9b847ab5b57fd2d127213ca41ebd880de1f553b7c23b", - "url": "https://files.pythonhosted.org/packages/3f/f2/2624e12ef854ee667d92ac5dc7815932095e0852e5ff2b2bf57feda8a11b/websocket-client-1.5.2.tar.gz" + "hash": "c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd", + "url": "https://files.pythonhosted.org/packages/b1/34/3a5cae1e07d9566ad073fa6d169bf22c03a3ba7b31b3c3422ec88d039108/websocket-client-1.6.1.tar.gz" } ], "project_name": "websocket-client", @@ -4299,7 +4304,7 @@ "wsaccel; extra == \"optional\"" ], "requires_python": ">=3.7", - "version": "1.5.2" + "version": "1.6.1" }, { "artifacts": [ @@ -4405,7 +4410,9 @@ "platform_tag": null } ], - "path_mappings": {}, + "path_mappings": { + "WHEELS_DIR": null + }, "pex_version": "2.1.134", "pip_version": "20.3.4-patched", "prefer_older_binary": false, From 2e04cfd6e2f584c7975bc60aa62f0540f66e3b68 Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 14:42:58 +0900 Subject: [PATCH 3/7] setup: Add the precompiled wheel of asyncudp --- vendor/wheelhouse/asyncudp-0.10.0-py3-none-any.whl | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 vendor/wheelhouse/asyncudp-0.10.0-py3-none-any.whl diff --git a/vendor/wheelhouse/asyncudp-0.10.0-py3-none-any.whl b/vendor/wheelhouse/asyncudp-0.10.0-py3-none-any.whl new file mode 100644 index 0000000000..59ebcc9749 --- /dev/null +++ b/vendor/wheelhouse/asyncudp-0.10.0-py3-none-any.whl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e653538c1d78192d6117095cd90f82a7a56816a21eb3e22456ffcf798791c11 +size 3539 From 92e1697662dcac8fa47d51dc84d05179bf2b8839 Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 14:45:46 +0900 Subject: [PATCH 4/7] Squashed 'vendor/temporenc/' content from commit 376ca068 git-subtree-dir: vendor/temporenc git-subtree-split: 376ca0686f5b1b86d0319871c8739dd2f4b338ec --- .gitignore | 14 + LICENSE.rst | 33 ++ Makefile | 21 + README.rst | 10 + doc/conf.py | 62 +++ doc/index.rst | 235 ++++++++++ pytest.ini | 2 + requirements-dev.txt | 3 + requirements-test.txt | 2 + setup.cfg | 3 + setup.py | 32 ++ temporenc/__init__.py | 16 + temporenc/temporenc.py | 933 ++++++++++++++++++++++++++++++++++++++++ tests/test_temporenc.py | 590 +++++++++++++++++++++++++ tox.ini | 6 + 15 files changed, 1962 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.rst create mode 100644 Makefile create mode 100644 README.rst create mode 100644 doc/conf.py create mode 100644 doc/index.rst create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 requirements-test.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 temporenc/__init__.py create mode 100644 temporenc/temporenc.py create mode 100644 tests/test_temporenc.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..4257374ea6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Packaging cruft +/*.egg-info/ + +# Byte code +__pycache__/ +*.py[co] + +# Testing cruft +/.tox/ +/.coverage +htmlcov/ + +# Documentation +/doc/build/ diff --git a/LICENSE.rst b/LICENSE.rst new file mode 100644 index 0000000000..89e3074e18 --- /dev/null +++ b/LICENSE.rst @@ -0,0 +1,33 @@ +License +======= + +Copyright © 2014–2017, Wouter Bolsterlee + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*(This is the OSI approved 3-clause "New BSD License".)* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..f2d85c58ed --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ + +.PHONY: all doc test + +all: + +doc: + @echo + @echo "Building documentation" + @echo "======================" + @echo + python setup.py build_sphinx + @echo + @echo Generated documentation: "file://"$$(readlink -f doc/build/html/index.html) + @echo + +test: + @echo + @echo "Running tests" + @echo "=============" + @echo + py.test tests/ diff --git a/README.rst b/README.rst new file mode 100644 index 0000000000..4a5b509c4d --- /dev/null +++ b/README.rst @@ -0,0 +1,10 @@ +==================== +Temporenc for Python +==================== + +This is a Python library implementing the `temporenc format +`_. + +* `Online documentation `_ +* `Project page (Github) `_ +* `Temporenc website `_ diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000000..bdac90690f --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,62 @@ +import datetime +import os + +import temporenc + + +# +# Project settings +# + +project = 'Temporenc' +now = datetime.datetime.now() +if now.year > 2014: + copyright = '2014-{0}, Wouter Bolsterlee'.format(now.year) +else: + copyright = '2014, Wouter Bolsterlee' +version = temporenc.__version__ +release = temporenc.__version__ + +# +# Extensions +# + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', +] + +autodoc_member_order = 'bysource' + +# +# Files and paths +# + +master_doc = 'index' +templates_path = ['_templates'] +source_suffix = '.rst' +exclude_patterns = ['build'] + + +# +# Output +# + +pygments_style = 'sphinx' +html_theme = 'default' +html_static_path = ['_static'] +html_domain_indices = False +html_use_index = False +html_show_sphinx = False +html_show_copyright = True + + +# +# These docs are intended for hosting by readthedocs.org. Override some +# settings for local use. +# + +if not 'READTHEDOCS' in os.environ: + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000000..6341b946d9 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,235 @@ +****************************** +Python library for *temporenc* +****************************** + +This is a Python library implementing the `temporenc format +`_ for dates and times. + +Features: + +* Support for all *temporenc* types + +* Interoperability with the ``datetime`` module + +* Time zone support, including conversion to local time + +* Compatibility with both Python 2 (2.6+) and Python 3 (3.2+) + +* Decent performance + +* Permissive BSD license + +____ + + +.. rubric:: Contents + +.. contents:: + :local: + +____ + + +Installation +============ + + +Use ``pip`` to install the library (e.g. into a ``virtualenv``): + +.. code-block:: shell-session + + $ pip install temporenc + +____ + + +Usage +===== + +.. py:currentmodule:: temporenc + +Basic usage +----------- + +All functionality is provided by a single module with the name ``temporenc``:: + + >>> import temporenc + +To encode date and time information into a byte string, use the :py:func:`packb` +function:: + + >>> temporenc.packb(year=2014, month=10, day=23) + b'\x8f\xbd6' + +This function automatically determines the most compact representation for the +provided information. In this case, the result uses *temporenc* type ``D``, but +if you want to use a different type, you can provide it explicitly:: + + >>> temporenc.packb(type='DT', year=2014, month=10, day=23) + b'\x1fzm\xff\xff' + +To unpack a byte string, use :py:func:`unpackb`:: + + >>> moment = temporenc.unpackb(b'\x1fzm\xff\xff') + >>> moment + + >>> print(moment) + 2014-10-23 + +As you can see, unpacking returns a :py:class:`Moment` instance. This class has +a reasonable string representation, but it is generally more useful to access +the individual components using one of its many attributes:: + + >>> print(moment.year) + 2014 + >>> print(moment.month) + 10 + >>> print(moment.day) + 13 + >>> print(moment.second) + None + +Since all fields are optional in *temporenc* values, and since no time +information was set in this example, some of the attributes (e.g. `second`) are +`None`. + +Integration with the ``datetime`` module +---------------------------------------- + +Python has built-in support for date and time handling, provided by the +``datetime`` module in the standard library, which is how applications usually +work with date and time information. Instead of specifying all the fields +manually when packing data, which is cumbersome and error-prone, the +``temporenc`` module integrates with the built-in ``datetime`` module:: + + >>> import datetime + >>> now = datetime.datetime.now() + >>> now + datetime.datetime(2014, 10, 23, 18, 45, 23, 612883) + >>> temporenc.packb(now) + b'W\xde\x9bJ\xd5\xe5hL' + +As you can see, instead of specifying all the components manually, instances of +the built-in ``datetime.datetime`` class can be passed directly as the first +argument to :py:func:`packb`. This also works for ``datetime.date`` and +``datetime.time`` instances. + +Since the Python ``datetime`` module *always* uses microsecond precision, this +library defaults to *temporenc* types with sub-second precision (e.g. ``DTS``) +when an instance of one of the ``datetime`` classes is passed. If no subsecond +precision is required, you can specify a different type to save space:: + + >>> temporenc.packb(now, type='DT') + b'\x1fzm+W' + +The integration with the ``datetime`` module works both ways. Instances of the +:py:class:`Moment` class (as returned by the unpacking functions) can be +converted to the standard date and time classes using the +:py:meth:`~Moment.datetime`, :py:meth:`~Moment.date`, and +:py:meth:`~Moment.time` methods:: + + >>> moment = temporenc.unpackb(b'W\xde\x9bJ\xd5\xe5hL') + >>> moment + + >>> moment.datetime() + datetime.datetime(2014, 10, 23, 18, 45, 23, 612883) + >>> moment.date() + datetime.date(2014, 10, 23) + >>> moment.time() + datetime.time(18, 45, 23, 612883) + +Conversion to and from classes from the ``datetime`` module have full time zone +support. See the API docs for :py:meth:`Moment.datetime` for more details about +time zone handling. + +.. warning:: + + The Python ``temporenc`` module only concerns itself with encoding and + decoding. It does *not* do any date and time calculations, and hence does not + validate that dates are correct. For example, it handles the non-existent + date `February 30` just fine. Always convert to native classes from the + ``datetime`` module if you need to work with date and time information in + your application. + + +Working with file-like objects +------------------------------ + +The *temporenc* encoding format allows for reading data from a stream without +knowing in advance how big the encoded byte string is. This library supports +this through the :py:func:`unpack` function, which consumes exactly the required +number of bytes from the stream:: + + >>> import io + >>> fp = io.BytesIO() # this could be a real file + >>> fp.write(b'W\xde\x9bJ\xd5\xe5hL') + >>> fp.write(b'foo') + >>> fp.seek(0) + >>> temporenc.unpack(fp) + + >>> fp.tell() + 8 + >>> fp.read() + b'foo' + +For writing directly to a file-like object, the :py:func:`pack` function can be +used, though this is just a shortcut. + +____ + + +API +=== + +The :py:func:`packb` and :py:func:`unpackb` functions operate on byte strings. + +.. autofunction:: packb +.. autofunction:: unpackb + +The :py:func:`pack` and :py:func:`unpack` functions operate on file-like +objects. + +.. autofunction:: pack +.. autofunction:: unpack + +Both :py:func:`unpackb` and :py:func:`unpack` return an instance of the +:py:class:`Moment` class. + +.. autoclass:: Moment + :members: + +____ + + +Contributing +============ + +Source code, including the test suite, is maintained at Github: + + `temporenc-python on github `_ + +Feel free to submit feedback, report issues, bring up improvement ideas, and +contribute fixes! + +____ + + +Version history +=============== + +* x.y (not yet released) + + * no longer perform utc conversion, see + `temporenc#8 `_ + +* 0.1 + + Release date: 2014-10-30 + + Initial public release. + +____ + +.. license is in a separate file + +.. include:: ../LICENSE.rst diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000..97607fa29c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --verbose --showlocals --cov temporenc --cov-report html --cov-report term-missing diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000000..f031cbad5f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +sphinx +sphinx-rtd-theme +tox diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000000..9955deccd9 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,2 @@ +pytest +pytest-cov diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..612fd61211 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[build_sphinx] +source-dir = doc/ +build-dir = doc/build/ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..af2fb21b71 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +import os +from setuptools import setup + +# No third party dependencies, so importing the package should be safe. +import temporenc + +with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as fp: + long_description = fp.read() + +setup( + name='temporenc', + description="Python library for the temporenc format", + long_description=long_description, + version=temporenc.__version__, + author="Wouter Bolsterlee", + author_email="uws@xs4all.nl", + url='https://github.com/wbolster/temporenc-python', + packages=['temporenc'], + license='BSD', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Database', + 'Topic :: Database :: Database Engines/Servers', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) diff --git a/temporenc/__init__.py b/temporenc/__init__.py new file mode 100644 index 0000000000..4c1a6a4465 --- /dev/null +++ b/temporenc/__init__.py @@ -0,0 +1,16 @@ +""" +Temporenc, a comprehensive binary encoding format for dates and times +""" + +__version__ = '0.1.0' +__version_info__ = tuple(map(int, __version__.split('.'))) + + +# Export public API +from .temporenc import ( # noqa + pack, + packb, + unpack, + unpackb, + Moment, +) diff --git a/temporenc/temporenc.py b/temporenc/temporenc.py new file mode 100644 index 0000000000..62b058ce8d --- /dev/null +++ b/temporenc/temporenc.py @@ -0,0 +1,933 @@ + +import datetime +import struct +import sys + + +# +# Compatibility +# + +PY2 = sys.version_info[0] == 2 +PY26 = sys.version_info[0:2] == (2, 6) + + +# +# Components and types +# + +SUPPORTED_TYPES = set(['D', 'T', 'DT', 'DTZ', 'DTS', 'DTSZ']) + +D_MASK = 0x1fffff +T_MASK = 0x1ffff +Z_MASK = 0x7f + +YEAR_MAX, YEAR_EMPTY, YEAR_MASK = 4094, 4095, 0xfff +MONTH_MAX, MONTH_EMPTY, MONTH_MASK = 11, 15, 0xf +DAY_MAX, DAY_EMPTY, DAY_MASK = 30, 31, 0x1f +HOUR_MAX, HOUR_EMPTY, HOUR_MASK = 23, 31, 0x1f +MINUTE_MAX, MINUTE_EMPTY, MINUTE_MASK = 59, 63, 0x3f +SECOND_MAX, SECOND_EMPTY, SECOND_MASK = 60, 63, 0x3f +MILLISECOND_MAX, MILLISECOND_MASK = 999, 0x3ff +MICROSECOND_MAX, MICROSECOND_MASK = 999999, 0xfffff +NANOSECOND_MAX, NANOSECOND_MASK = 999999999, 0x3fffffff +TIMEZONE_MAX, TIMEZONE_EMPTY = 125, 127 + +D_LENGTH = 3 +T_LENGTH = 3 +DT_LENGTH = 5 +DTZ_LENGTH = 6 +DTS_LENGTHS = [7, 8, 9, 6] # indexed by precision bits +DTSZ_LENGTHS = [8, 9, 10, 7] # idem + + +# +# Helpers +# + +pack_4 = struct.Struct('>L').pack +pack_8 = struct.Struct('>Q').pack +pack_2_8 = struct.Struct('>HQ').pack + + +def unpack_4(value, _unpack=struct.Struct('>L').unpack): + return _unpack(value)[0] + + +def unpack_8(value, _unpack=struct.Struct('>Q').unpack): + return _unpack(value)[0] + + +def _detect_type(first): + """ + Detect type information from the numerical value of the first byte. + """ + if first <= 0b00111111: + return 'DT', None, DT_LENGTH + elif first <= 0b01111111: + precision = first >> 4 & 0b11 + return 'DTS', precision, DTS_LENGTHS[precision] + elif first <= 0b10011111: + return 'D', None, D_LENGTH + elif first <= 0b10100001: + return 'T', None, T_LENGTH + elif first <= 0b10111111: + return None, None, None + elif first <= 0b11011111: + return 'DTZ', None, DTZ_LENGTH + elif first <= 0b11111111: + precision = first >> 3 & 0b11 + return 'DTSZ', precision, DTSZ_LENGTHS[precision] + + +class FixedOffset(datetime.tzinfo): + """Time zone information for a fixed offset from UTC.""" + + # Python 2 does not have any concrete tzinfo implementations in its + # standard library, hence this implementation. This implementation + # is based on the examples in the Python docs, in particular: + # https://docs.python.org/3.4/library/datetime.html#tzinfo-objects + + ZERO = datetime.timedelta(0) + + def __init__(self, minutes): + self._offset = datetime.timedelta(minutes=minutes) + sign = '+' if minutes >= 0 else '-' + hours, minutes = divmod(minutes, 60) + self._name = 'UTC{0}{1:02d}:{2:02d}'.format(sign, hours, minutes) + + def utcoffset(self, dt): + return self._offset + + def tzname(self, dt): + return self._name + + def dst(self, dt): + return self.ZERO + + def __repr__(self): + return '<{0}>'.format(self._name) + + +# This cache maps offsets in minutes to FixedOffset instances. +tzinfo_cache = { + None: None, # hack to simpify cached_tzinfo() callers +} + + +def cached_tzinfo(minutes): + """ + Get a (cached) tzinfo instance for the specified offset in minutes. + """ + try: + tzinfo = tzinfo_cache[minutes] + except KeyError: + tzinfo_cache[minutes] = tzinfo = FixedOffset(minutes) + return tzinfo + + +# +# Public API +# + +class Moment(object): + """ + Container to represent a parsed *temporenc* value. + + Each constituent part is accessible as an instance attribute. These + are: ``year``, ``month``, ``day``, ``hour``, ``minute``, ``second``, + ``millisecond``, ``microsecond``, ``nanosecond``, and ``tz_offset``. + Since *temporenc* allows partial date and time information, any + attribute can be ``None``. + + The attributes for sub-second precision form a group that is either + completely empty (all attributes are ``None``) or completely filled + (no attribute is ``None``). + + This class is intended to be a read-only immutable data structure; + assigning new values to attributes is not supported. + + Instances are hashable and can be used as dictionary keys or as + members of a set. Instances representing the same moment in time + have the same hash value. Time zone information is not taken into + account for hashing purposes, since time zone aware values must have + their constituent parts in UTC. + + Instances of this class can be compared to each other, with earlier + dates sorting first. As with hashing, time zone information is not + taken into account, since the actual data must be in UTC in those + cases. + + .. note:: + + This class must not be instantiated directly; use one of the + unpacking functions like :py:func:`unpackb()` instead. + """ + __slots__ = [ + 'year', 'month', 'day', + 'hour', 'minute', 'second', + 'millisecond', 'microsecond', 'nanosecond', + 'tz_offset', + '_has_date', '_has_time', '_struct'] + + def __init__( + self, + year, month, day, + hour, minute, second, nanosecond, + tz_offset): + + #: Year component. + self.year = year + + #: Month component. + self.month = month + + #: Day component. + self.day = day + + #: Hour component. + self.hour = hour + + #: Minute component. + self.minute = minute + + #: Second component. + self.second = second + + if nanosecond is None: + self.millisecond = self.microsecond = self.nanosecond = None + else: + #: Millisecond component. If set, :py:attr:`microsecond` and + #: :py:attr:`nanosecond` are also set. + self.millisecond = nanosecond // 1000000 + + #: Microsecond component. If set, :py:attr:`millisecond` and + #: :py:attr:`nanosecond` are also set. + self.microsecond = nanosecond // 1000 + + #: Nanosecond component. If set, :py:attr:`millisecond` and + #: :py:attr:`microsecond` are also set. + self.nanosecond = nanosecond + + #: Time zone offset (total minutes). To calculate the hours and + #: minutes, use ``h, m = divmod(offset, 60)``. + self.tz_offset = tz_offset + + self._has_date = not (year is None and month is None and day is None) + self._has_time = not (hour is None and minute is None + and second is None) + + # This 'struct' contains the values that are relevant for + # comparison, hashing, and so on. + self._struct = ( + self.year, self.month, self.day, + self.hour, self.minute, self.second, self.nanosecond, + self.tz_offset) + + def __str__(self): + buf = [] + + if self._has_date: + buf.append("{0:04d}-".format(self.year) + if self.year is not None else "????-") + buf.append("{0:02d}-".format(self.month) + if self.month is not None else "??-") + buf.append("{0:02d}".format(self.day) + if self.day is not None else "??") + + if self._has_time: + + if self._has_date: + buf.append(" ") # separator + + buf.append("{0:02d}:".format(self.hour) + if self.hour is not None else "??:") + buf.append("{0:02d}:".format(self.minute) + if self.minute is not None else "??:") + buf.append("{0:02d}".format(self.second) + if self.second is not None else "??") + + if self.nanosecond is not None: + if not self._has_time: + # Weird edge case: empty hour/minute/second, but + # sub-second precision is set. + buf.append("??:??:??") + + if self.nanosecond == 0: + buf.append('.0') + else: + buf.append(".{0:09d}".format(self.nanosecond).rstrip("0")) + + if self.tz_offset is not None: + if self.tz_offset == 0: + buf.append('Z') + else: + h, m = divmod(self.tz_offset, 60) + sign = '+' if h >= 0 else '-' + buf.append('{0}{1:02d}:{2:02d}'.format(sign, h, m)) + + return ''.join(buf) + + def __repr__(self): + return "".format(self) + + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + return self._struct == other._struct + + def __ne__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + return self._struct != other._struct + + def __gt__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + return self._struct > other._struct + + def __ge__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + return self._struct >= other._struct + + def __lt__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + return self._struct < other._struct + + def __le__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + return self._struct <= other._struct + + def __hash__(self): + return hash(self._struct) + + def datetime(self, strict=True): + """ + Convert this value to a ``datetime.datetime`` instance. + + Since the classes in the ``datetime`` module do not support + missing values, this will fail when one of the required + components is not set, which is indicated by raising + a :py:exc:`ValueError`. + + The default is to perform a strict conversion. To ease working + with partial dates and times, the `strict` argument can be set + to `False`. In that case this method will try to convert the + value anyway, by substituting a default value for any missing + component, e.g. a missing time is set to `00:00:00`. Note that + these substituted values are bogus and should not be used for + any application logic, but at least this allows applications to + use things like ``.strftime()`` on partial dates and times. + + The *temporenc* format allows inclusion of a time zone offset. + When converting to a ``datetime`` instance, time zone + information is handled as follows: + + * When no time zone information was present in the original data + (e.g. when unpacking *temporenc* type ``DT``), the return + value will be a naive `datetime` instance, i.e. its ``tzinfo`` + attribute is `None`. + + * If the original data did include time zone information, the + return value will be a time zone aware instance, which means the + instance will have a ``tzinfo`` attribute corresponding to + the offset included in the value. + + :param bool strict: whether to use strict conversion rules + :return: converted value + :rtype: `datetime.datetime` + """ + + if strict: + if None in (self.year, self.month, self.day): + raise ValueError("incomplete date information") + if None in (self.hour, self.minute, self.second): + raise ValueError("incomplete time information") + + hour, minute, second = self.hour, self.minute, self.second + year, month, day = self.year, self.month, self.day + + # The stdlib's datetime classes always specify microseconds. + us = self.microsecond if self.microsecond is not None else 0 + + if not strict: + # Substitute defaults for missing values. + if year is None: + year = 1 + + if month is None: + month = 1 + + if day is None: + day = 1 + + if hour is None: + hour = 0 + + if minute is None: + minute = 0 + + if second is None: + second = 0 + elif second == 60: # assume that this is a leap second + second = 59 + + dt = datetime.datetime( + year, month, day, + hour, minute, second, us, + tzinfo=cached_tzinfo(self.tz_offset)) + + return dt + + def date(self, strict=True): + """ + Convert this value to a ``datetime.date`` instance. + + See the documentation for the :py:meth:`datetime()` method for + more information. + + :param bool strict: whether to use strict conversion rules + :param bool local: whether to convert to local time + :return: converted value + :rtype: `datetime.date` + """ + if strict: + if None in (self.year, self.month, self.day): + raise ValueError("incomplete date information") + + # Shortcut for performance reasons + return datetime.date(self.year, self.month, self.day) + + return self.datetime(strict=False).date() + + def time(self, strict=True): + """ + Convert this value to a ``datetime.time`` instance. + + See the documentation for the :py:meth:`datetime()` method for + more information. + + :param bool strict: whether to use strict conversion rules + :param bool local: whether to convert to local time + :return: converted value + :rtype: `datetime.time` + """ + if strict: + if None in (self.hour, self.minute, self.second): + raise ValueError("incomplete time information") + + # Shortcut for performance reasons + return datetime.time( + self.hour, self.minute, self.second, + self.microsecond if self.microsecond is not None else 0, + tzinfo=cached_tzinfo(self.tz_offset)) + + return self.datetime(strict=False).timetz() + + +def packb( + value=None, type=None, + year=None, month=None, day=None, + hour=None, minute=None, second=None, + millisecond=None, microsecond=None, nanosecond=None, + tz_offset=None): + """ + Pack date and time information into a byte string. + + If specified, `value` must be a ``datetime.datetime``, + ``datetime.date``, or ``datetime.time`` instance. + + The `type` specifies the *temporenc* type to use. Valid types are + ``D``, ``T``, ``DT``, ``DTZ``, ``DTS``, or ``DTSZ``. If not + specified, the most compact encoding that can represent the provided + information will be determined automatically. Note that instances of + the classes in the ``datetime`` module always use microsecond + precision, so make sure to specify a more compact type if no + sub-second precision is required. + + Most applications would only use the `value` and `type` arguments; + the other arguments allow for encoding data that does not fit the + conceptual date and time model used by the standard library's + ``datetime`` module. + + .. note:: + + Applications that require lexicographical ordering of encoded + values should always explicitly specify a type to use. + + All other arguments can be used to specify individual pieces of + information that make up a date or time. If both `value` and more + specific fields are provided, the individual fields override the + values extracted from `value`, e.g. ``packb(datetime.datetime.now(), + minute=0, second=0)`` encodes the start of the current hour. + + The sub-second precision arguments (`millisecond`, `microsecond`, + and `nanosecond`) must not be used together, since those are + conceptually mutually exclusive. + + .. note:: + + The `value` argument is the only positional argument. All other + arguments *must* be specified as keyword arguments (even though + this is not enforced because of Python 2 compatibility). + + :param value: instance of one of the ``datetime`` classes (optional) + :param str type: *temporenc* type (optional) + :param int year: year (optional) + :param int month: month (optional) + :param int day: day (optional) + :param int hour: hour (optional) + :param int minute: minute (optional) + :param int second: second (optional) + :param int millisecond: millisecond (optional) + :param int microsecond: microsecond (optional) + :param int nanosecond: nanosecond (optional) + :param int tz_offset: time zone offset in minutes from UTC (optional) + :return: encoded *temporenc* value + :rtype: bytes + """ + + # + # Native 'datetime' module handling + # + + if value is not None: + handled = False + + if isinstance(value, (datetime.datetime, datetime.time)): + + # Handle time zone information. Instances of the + # datetime.datetime and datetime.time classes may have an + # associated time zone. If an explicit tz_offset was + # specified, that takes precedence. + if tz_offset is None: + delta = value.utcoffset() + if delta is not None: + tz_offset = delta.days * 1440 + delta.seconds // 60 + + # Extract time fields + handled = True + if hour is None: + hour = value.hour + if minute is None: + minute = value.minute + if second is None: + second = value.second + if (millisecond is None and microsecond is None + and nanosecond is None): + microsecond = value.microsecond + + if isinstance(value, (datetime.datetime, datetime.date)): + # Extract date fields + handled = True + if year is None: + year = value.year + if month is None: + month = value.month + if day is None: + day = value.day + + if not handled: + raise ValueError("Cannot encode {0!r}".format(value)) + + # + # Type detection + # + + if type is None: + has_d = not (year is None and month is None and day is None) + has_t = not (hour is None and minute is None and second is None) + has_s = not (millisecond is None and microsecond is None + and nanosecond is None) + has_z = tz_offset is not None + + if has_z and has_s: + type = 'DTSZ' + elif has_z: + type = 'DTZ' + elif has_s: + type = 'DTS' + elif has_d and has_t: + type = 'DT' + elif has_d: + type = 'D' + elif has_t: + type = 'T' + else: + # No information at all, just use the smallest type + type = 'D' + + elif type not in SUPPORTED_TYPES: + raise ValueError("invalid temporenc type: {0!r}".format(type)) + + # + # Value checking + # + + if year is None: + year = YEAR_EMPTY + elif not 0 <= year <= YEAR_MAX: + raise ValueError("year not within supported range") + + if month is None: + month = MONTH_EMPTY + else: + month -= 1 + if not 0 <= month <= MONTH_MAX: + raise ValueError("month not within supported range") + + if day is None: + day = DAY_EMPTY + else: + day -= 1 + if not 0 <= day <= DAY_MAX: + raise ValueError("day not within supported range") + + if hour is None: + hour = HOUR_EMPTY + elif not 0 <= hour <= HOUR_MAX: + raise ValueError("hour not within supported range") + + if minute is None: + minute = MINUTE_EMPTY + elif not 0 <= minute <= MINUTE_MAX: + raise ValueError("minute not within supported range") + + if second is None: + second = SECOND_EMPTY + elif not 0 <= second <= SECOND_MAX: + raise ValueError("second not within supported range") + + if (millisecond is not None + and not 0 <= millisecond <= MILLISECOND_MAX): + raise ValueError("millisecond not within supported range") + + if (microsecond is not None + and not 0 <= microsecond <= MICROSECOND_MAX): + raise ValueError("microsecond not within supported range") + + if (nanosecond is not None + and not 0 <= nanosecond <= NANOSECOND_MAX): + raise ValueError("nanosecond not within supported range") + + if tz_offset is None: + tz_offset = TIMEZONE_EMPTY + else: + z, remainder = divmod(tz_offset, 15) + if remainder: + raise ValueError("tz_offset must be a multiple of 15") + z += 64 + if not 0 <= z <= TIMEZONE_MAX: + raise ValueError("tz_offset not within supported range") + + # + # Byte packing + # + + d = year << 9 | month << 5 | day + t = hour << 12 | minute << 6 | second + + if type == 'D': + # 100DDDDD DDDDDDDD DDDDDDDD + return pack_4(0b100 << 21 | d)[-3:] + + elif type == 'T': + # 1010000T TTTTTTTT TTTTTTTT + return pack_4(0b1010000 << 17 | t)[-3:] + + elif type == 'DT': + # 00DDDDDD DDDDDDDD DDDDDDDT TTTTTTTT + # TTTTTTTT + return pack_8(d << 17 | t)[-5:] + + elif type == 'DTZ': + # 110DDDDD DDDDDDDD DDDDDDDD TTTTTTTT + # TTTTTTTT TZZZZZZZ + return pack_8(0b110 << 45 | d << 24 | t << 7 | z)[-6:] + + elif type == 'DTS': + if nanosecond is not None: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSSSS + # SSSSSSSS + return pack_2_8( + 0b0110 << 4 | d >> 17, + (d & 0x1ffff) << 47 | t << 30 | nanosecond)[-9:] + elif microsecond is not None: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSS00 + return pack_8( + 0b0101 << 60 | d << 39 | t << 22 | microsecond << 2) + elif millisecond is not None: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TTSSSSSS SSSS0000 + return pack_8( + 0b0100 << 52 | d << 31 | t << 14 | millisecond << 4)[-7:] + else: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TT000000 + return pack_8(0b0111 << 44 | d << 23 | t << 6)[-6:] + + elif type == 'DTSZ': + if nanosecond is not None: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSS + # SSSSSSSS SZZZZZZZ + return pack_2_8( + 0b11110 << 11 | d >> 10, + (d & 0x3ff) << 54 | t << 37 | nanosecond << 7 | z) + elif microsecond is not None: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSZ + # ZZZZZZ00 + return pack_2_8( + 0b11101 << 3 | d >> 18, + (d & 0x3ffff) << 46 | t << 29 | microsecond << 9 | z << 2)[-9:] + elif millisecond is not None: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTSSSSS SSSSSZZZ ZZZZ0000 + return pack_8( + 0b11100 << 59 | d << 38 | t << 21 | millisecond << 11 | z << 4) + else: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTZZZZZ ZZ000000 + return pack_8(0b11111 << 51 | d << 30 | t << 13 | z << 6)[-7:] + + +def pack(fp, *args, **kwargs): + """ + Pack date and time information and write it to a file-like object. + + This is a short-hand for writing a packed value directly to + a file-like object. There is no additional behaviour. This function + only exists for API parity with the :py:func:`unpack()` function. + + Except for the first argument (the file-like object), all arguments + (both positional and keyword) are passed on to :py:func:`packb()`. + See :py:func:`packb()` for more information. + + :param file-like fp: writeable file-like object + :param args: propagated to :py:func:`packb()` + :param kwargs: propagated to :py:func:`packb()` + :return: number of bytes written + :rtype: int + """ + return fp.write(packb(*args, **kwargs)) + + +def unpackb(value): + """ + Unpack a *temporenc* value from a byte string. + + If no valid value could be read, this raises :py:exc:`ValueError`. + + :param bytes value: a byte string (or `bytearray`) to parse + :return: a parsed *temporenc* structure + :rtype: :py:class:`Moment` + """ + + # + # Unpack components + # + + first = value[0] + + if PY2 and isinstance(first, bytes): # pragma: no cover + first = ord(first) + + if PY26 and isinstance(value, bytearray): # pragma: no cover + # struct.unpack() does not handle bytearray() in Python < 2.7 + value = bytes(value) + + type, precision, expected_length = _detect_type(first) + + if type is None: + raise ValueError("first byte does not contain a valid tag") + + if len(value) != expected_length: + if precision is None: + raise ValueError( + "{0} value must be {1:d} bytes; got {2:d}".format( + type, expected_length, len(value))) + else: + raise ValueError( + "{0} value with precision {1:02b} must be {2:d} bytes; " + "got {3:d}".format( + type, precision, expected_length, len(value))) + + date = time = tz_offset = nanosecond = padding = None + + if type == 'D': + # 100DDDDD DDDDDDDD DDDDDDDD + date = unpack_4(b'\x00' + value) & D_MASK + + elif type == 'T': + # 1010000T TTTTTTTT TTTTTTTT + time = unpack_4(b'\x00' + value) & T_MASK + + elif type == 'DT': + # 00DDDDDD DDDDDDDD DDDDDDDT TTTTTTTT + # TTTTTTTT + n = unpack_8(b'\x00\x00\x00' + value) + date = n >> 17 & D_MASK + time = n & T_MASK + + elif type == 'DTZ': + # 110DDDDD DDDDDDDD DDDDDDDD TTTTTTTT + # TTTTTTTT TZZZZZZZ + n = unpack_8(b'\x00\x00' + value) + date = n >> 24 & D_MASK + time = n >> 7 & T_MASK + tz_offset = n & Z_MASK + + elif type == 'DTS': + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TT...... (first 6 bytes) + n = unpack_8(b'\x00\x00' + value[:6]) >> 6 + date = n >> 17 & D_MASK + time = n & T_MASK + + # Extract S component from last 4 bytes + n = unpack_4(value[-4:]) + if precision == 0b00: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TTSSSSSS SSSS0000 + nanosecond = (n >> 4 & MILLISECOND_MASK) * 1000000 + padding = n & 0b1111 + elif precision == 0b01: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSS00 + nanosecond = (n >> 2 & MICROSECOND_MASK) * 1000 + padding = n & 0b11 + elif precision == 0b10: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TTSSSSSS SSSSSSSS SSSSSSSS + # SSSSSSSS + nanosecond = n & NANOSECOND_MASK + elif precision == 0b11: + # 01PPDDDD DDDDDDDD DDDDDDDD DTTTTTTT + # TTTTTTTT TT000000 + padding = n & 0b111111 + + elif type == 'DTSZ': + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTT..... (first 6 bytes) + n = unpack_8(b'\x00\x00' + value[:6]) >> 5 + date = n >> 17 & D_MASK + time = n & T_MASK + + # Extract S and Z components from last 5 bytes + n = unpack_8(b'\x00\x00\x00' + value[-5:]) + if precision == 0b00: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTSSSSS SSSSSZZZ ZZZZ0000 + nanosecond = (n >> 11 & MILLISECOND_MASK) * 1000000 + tz_offset = n >> 4 & Z_MASK + padding = n & 0b1111 + elif precision == 0b01: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSZ + # ZZZZZZ00 + nanosecond = (n >> 9 & MICROSECOND_MASK) * 1000 + tz_offset = n >> 2 & Z_MASK + padding = n & 0b11 + elif precision == 0b10: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTSSSSS SSSSSSSS SSSSSSSS + # SSSSSSSS SZZZZZZZ + nanosecond = n >> 7 & NANOSECOND_MASK + tz_offset = n & Z_MASK + elif precision == 0b11: + # 111PPDDD DDDDDDDD DDDDDDDD DDTTTTTT + # TTTTTTTT TTTZZZZZ ZZ000000 + tz_offset = n >> 6 & Z_MASK + padding = n & 0b111111 + + if padding: + raise ValueError("padding bits must be zero") + + # + # Split D and T components + # + + if date is None: + year = month = day = None + else: + year = date >> 9 & YEAR_MASK # always within range + if year == YEAR_EMPTY: + year = None + + month = date >> 5 & MONTH_MASK + if month == MONTH_EMPTY: + month = None + elif month > MONTH_MAX: + raise ValueError("month not within supported range") + else: + month += 1 + + day = date & DAY_MASK # always within range + if day == DAY_EMPTY: + day = None + else: + day += 1 + + if time is None: + hour = minute = second = None + else: + hour = time >> 12 & HOUR_MASK + if hour == HOUR_EMPTY: + hour = None + elif hour > HOUR_MAX: + raise ValueError("hour not within supported range") + + minute = time >> 6 & MINUTE_MASK + if minute == MINUTE_EMPTY: + minute = None + elif minute > MINUTE_MAX: + raise ValueError("minute not within supported range") + + second = time & SECOND_MASK + if second == SECOND_EMPTY: + second = None + elif second > SECOND_MAX: + raise ValueError("second not within supported range") + + # + # Normalize time zone offset + # + + if tz_offset is not None: + tz_offset = 15 * (tz_offset - 64) + + # + # Sub-second fields are either all None, or none are None. + # + + if nanosecond is not None and nanosecond > NANOSECOND_MAX: + raise ValueError("sub-second precision not within supported range") + + return Moment( + year, month, day, + hour, minute, second, nanosecond, + tz_offset) + + +def unpack(fp): + """ + Unpack a *temporenc* value from a file-like object. + + This function consumes exactly the number of bytes required to + unpack a single *temporenc* value. + + If no valid value could be read, this raises :py:exc:`ValueError`. + + :param file-like fp: readable file-like object + :return: a parsed *temporenc* structure + :rtype: :py:class:`Moment` + """ + first = fp.read(1) + _, _, size = _detect_type(ord(first)) + return unpackb(first + fp.read(size - 1)) diff --git a/tests/test_temporenc.py b/tests/test_temporenc.py new file mode 100644 index 0000000000..135c378a5d --- /dev/null +++ b/tests/test_temporenc.py @@ -0,0 +1,590 @@ +import binascii +import datetime +import io +import operator +import sys + +import pytest + +import temporenc + +PY2 = sys.version_info[0] == 2 + + +def from_hex(s): + """Compatibility helper like bytes.fromhex() in Python 3""" + return binascii.unhexlify(s.replace(' ', '').encode('ascii')) + + +def test_type_d(): + + actual = temporenc.packb(type='D', year=1983, month=1, day=15) + expected = from_hex('8f 7e 0e') + assert actual == expected + + v = temporenc.unpackb(expected) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (None, None, None) + + +def test_type_t(): + + actual = temporenc.packb(type='T', hour=18, minute=25, second=12) + expected = from_hex('a1 26 4c') + assert actual == expected + + v = temporenc.unpackb(expected) + assert (v.year, v.month, v.day) == (None, None, None) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + + +def test_type_dt(): + + actual = temporenc.packb( + type='DT', + year=1983, month=1, day=15, + hour=18, minute=25, second=12) + expected = from_hex('1e fc 1d 26 4c') + assert actual == expected + + v = temporenc.unpackb(expected) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + + +def test_type_dtz(): + + actual = temporenc.packb( + type='DTZ', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, + tz_offset=60) + expected = from_hex('cf 7e 0e 93 26 44') + assert actual == expected + + v = temporenc.unpackb(expected) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + assert v.tz_offset == 60 + + +def test_type_dts(): + + actual = temporenc.packb( + type='DTS', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, millisecond=123) + dts_ms = from_hex('47 bf 07 49 93 07 b0') + assert actual == dts_ms + v = temporenc.unpackb(dts_ms) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + assert v.millisecond == 123 + assert v.microsecond == 123000 + assert v.nanosecond == 123000000 + + actual = temporenc.packb( + type='DTS', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, microsecond=123456) + dts_us = from_hex('57 bf 07 49 93 07 89 00') + assert actual == dts_us + v = temporenc.unpackb(dts_us) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + assert v.millisecond == 123 + assert v.microsecond == 123456 + assert v.nanosecond == 123456000 + + actual = temporenc.packb( + type='DTS', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, nanosecond=123456789) + dts_ns = from_hex('67 bf 07 49 93 07 5b cd 15') + assert actual == dts_ns + v = temporenc.unpackb(dts_ns) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + assert v.millisecond == 123 + assert v.microsecond == 123456 + assert v.nanosecond == 123456789 + + actual = temporenc.packb( + type='DTS', + year=1983, month=1, day=15, + hour=18, minute=25, second=12) + dts_none = from_hex('77 bf 07 49 93 00') + assert actual == dts_none + v = temporenc.unpackb(dts_none) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + assert v.millisecond is None + assert v.microsecond is None + assert v.nanosecond is None + + +def test_type_dtsz(): + + actual = temporenc.packb( + type='DTSZ', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, millisecond=123, + tz_offset=60) + dtsz_ms = from_hex('e3 df 83 a4 c9 83 dc 40') + assert actual == dtsz_ms + v = temporenc.unpackb(dtsz_ms) + assert (v.year, v.month, v.day) == (1983, 1, 15) + assert (v.hour, v.minute, v.second) == (18, 25, 12) + assert v.millisecond == 123 + assert v.microsecond == 123000 + assert v.nanosecond == 123000000 + assert v.tz_offset == 60 + + actual = temporenc.packb( + type='DTSZ', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, microsecond=123456, + tz_offset=60) + dtsz_us = from_hex('eb df 83 a4 c9 83 c4 81 10') + assert actual == dtsz_us + assert temporenc.unpackb(dtsz_us).microsecond == 123456 + assert v.tz_offset == 60 + + actual = temporenc.packb( + type='DTSZ', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, nanosecond=123456789, + tz_offset=60) + dtsz_ns = from_hex('f3 df 83 a4 c9 83 ad e6 8a c4') + assert actual == dtsz_ns + assert temporenc.unpackb(dtsz_ns).nanosecond == 123456789 + assert v.tz_offset == 60 + + actual = temporenc.packb( + type='DTSZ', + year=1983, month=1, day=15, + hour=18, minute=25, second=12, + tz_offset=60) + dtsz_none = from_hex('fb df 83 a4 c9 91 00') + assert actual == dtsz_none + v = temporenc.unpackb(dtsz_none) + assert v.millisecond is None + assert v.millisecond is None + assert v.millisecond is None + assert v.tz_offset == 60 + + +def test_type_detection(): + + # Empty value, so should result in the smallest type + assert len(temporenc.packb()) == 3 + + # Type D + assert len(temporenc.packb(year=1983)) == 3 + assert temporenc.unpackb(temporenc.packb(year=1983)).year == 1983 + + # Type T + assert len(temporenc.packb(hour=18)) == 3 + assert temporenc.unpackb(temporenc.packb(hour=18)).hour == 18 + + # Type DT + assert len(temporenc.packb(year=1983, hour=18)) == 5 + + # Type DTS + assert len(temporenc.packb(millisecond=0)) == 7 + assert len(temporenc.packb(microsecond=0)) == 8 + assert len(temporenc.packb(nanosecond=0)) == 9 + + # Type DTZ + assert len(temporenc.packb(year=1983, hour=18, tz_offset=120)) == 6 + + # Type DTSZ + assert len(temporenc.packb(millisecond=0, tz_offset=120)) == 8 + + +def test_type_empty_values(): + v = temporenc.unpackb(temporenc.packb(type='DTS')) + assert (v.year, v.month, v.day) == (None, None, None) + assert (v.hour, v.minute, v.second) == (None, None, None) + assert (v.millisecond, v.microsecond, v.nanosecond) == (None, None, None) + assert v.tz_offset is None + + +def test_incorrect_sizes(): + + # Too long + with pytest.raises(ValueError): + temporenc.unpackb(temporenc.packb(year=1983) + b'foo') + with pytest.raises(ValueError): + temporenc.unpackb(temporenc.packb(millisecond=0) + b'foo') + + # Too short + with pytest.raises(ValueError): + temporenc.unpackb(temporenc.packb(year=1983)[:-1]) + with pytest.raises(ValueError): + temporenc.unpackb(temporenc.packb(millisecond=0)[:-1]) + + +def test_unpack_bytearray(): + ba = bytearray((0x8f, 0x7e, 0x0e)) + assert temporenc.unpackb(ba) is not None + + +def test_stream_unpacking(): + # This stream contains two values and one byte of trailing data + fp = io.BytesIO(from_hex('8f 7e 0e 8f 7e 0f ff')) + assert temporenc.unpack(fp).day == 15 + assert fp.tell() == 3 + assert temporenc.unpack(fp).day == 16 + assert fp.tell() == 6 + assert fp.read() == b'\xff' + + +def test_stream_packing(): + fp = io.BytesIO() + assert temporenc.pack(fp, year=1983) == 3 + assert temporenc.pack(fp, year=1984) == 3 + assert fp.tell() == 6 + assert len(fp.getvalue()) == 6 + + +def test_wrong_type(): + with pytest.raises(ValueError): + temporenc.packb(type="foo", year=1983) + + +def test_out_of_range_values(): + with pytest.raises(ValueError): + temporenc.packb(year=123456) + + with pytest.raises(ValueError): + temporenc.packb(month=-12) + + with pytest.raises(ValueError): + temporenc.packb(day=1234) + + with pytest.raises(ValueError): + temporenc.packb(hour=1234) + + with pytest.raises(ValueError): + temporenc.packb(minute=1234) + + with pytest.raises(ValueError): + temporenc.packb(second=1234) + + with pytest.raises(ValueError): + temporenc.packb(millisecond=1000) + + with pytest.raises(ValueError): + temporenc.packb(microsecond=1000000) + + with pytest.raises(ValueError): + temporenc.packb(nanosecond=10000000000) + + with pytest.raises(ValueError): + temporenc.packb(tz_offset=1050) + + with pytest.raises(ValueError): + temporenc.packb(tz_offset=13) # not a full quarter + + +def test_unpacking_bogus_data(): + with pytest.raises(ValueError) as e: + # First byte can never occur in valid values. + temporenc.unpackb(from_hex('bb 12 34')) + assert 'tag' in str(e.value) + + with pytest.raises(ValueError) as e: + temporenc.unpackb(from_hex('47 bf 07 49 93 07 b2')) + assert 'padding' in str(e.value) + + +def test_range_check_unpacking(): + + # Type T with out of range hour + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b10100001, 0b11100000, 0b00000000))) + assert 'hour' in str(e.value) + + # Type T with out of range minute + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b10100000, 0b00001111, 0b01000000))) + assert 'minute' in str(e.value) + + # Type T with out of range second + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b10100000, 0b00000000, 0b00111110))) + assert 'second' in str(e.value) + + # Type D with out of range month + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b10000000, 0b00000001, 0b11000000))) + assert 'month' in str(e.value) + + # Type DTS with out of range millisecond + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b01000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00111111, 0b11110000))) + assert 'sub-second' in str(e.value) + + # Type DTS with out of range microsecond + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b01010000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00111111, 0b11111111, 0b11111100))) + assert 'sub-second' in str(e.value) + + # Type DTS with out of range nanosecond + with pytest.raises(ValueError) as e: + temporenc.unpackb(bytearray(( + 0b01100000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00111111, 0b11111111, 0b11111111, + 0b11111111))) + assert 'sub-second' in str(e.value) + + +def test_native_packing(): + + with pytest.raises(ValueError): + temporenc.packb(object()) + + # datetime.date => D + actual = temporenc.packb(datetime.date(1983, 1, 15)) + expected = from_hex('8f 7e 0e') + assert actual == expected + + # datetime.datetime => DTS, unless told otherwise + actual = temporenc.packb(datetime.datetime( + 1983, 1, 15, 18, 25, 12, 123456)) + expected = from_hex('57 bf 07 49 93 07 89 00') + assert actual == expected + + actual = temporenc.packb( + datetime.datetime(1983, 1, 15, 18, 25, 12), + type='DT') + expected = from_hex('1e fc 1d 26 4c') + assert actual == expected + + # datetime.time => DTS, unless told otherwise + assert len(temporenc.packb(datetime.datetime.now().time())) == 8 + actual = temporenc.packb( + datetime.time(18, 25, 12), + type='T') + expected = from_hex('a1 26 4c') + assert actual == expected + + +def test_native_packing_with_overrides(): + actual = temporenc.packb( + datetime.datetime(1984, 1, 16, 18, 26, 12, 123456), + year=1983, day=15, minute=25) + expected = from_hex('57 bf 07 49 93 07 89 00') + assert actual == expected + + +def test_native_unpacking(): + value = temporenc.unpackb(temporenc.packb( + year=1983, month=1, day=15)) + assert value.date() == datetime.date(1983, 1, 15) + + value = temporenc.unpackb(temporenc.packb( + year=1983, month=1, day=15, + hour=1, minute=2, second=3, microsecond=456)) + assert value.datetime() == datetime.datetime(1983, 1, 15, 1, 2, 3, 456) + + value = temporenc.unpackb(temporenc.packb( + year=1983, month=1, day=15, # will be ignored + hour=1, minute=2, second=3, microsecond=456)) + assert value.time() == datetime.time(1, 2, 3, 456) + + value = temporenc.unpackb(temporenc.packb(year=1234)) + with pytest.raises(ValueError): + value.date() + assert value.date(strict=False).year == 1234 + assert value.datetime(strict=False).year == 1234 + + value = temporenc.unpackb(temporenc.packb(hour=14)) + with pytest.raises(ValueError): + value.time() + assert value.time(strict=False).hour == 14 + assert value.datetime(strict=False).hour == 14 + + +def test_native_unpacking_leap_second(): + value = temporenc.unpackb(temporenc.packb( + year=2013, month=6, day=30, + hour=23, minute=59, second=60)) + + with pytest.raises(ValueError): + value.datetime() # second out of range + + dt = value.datetime(strict=False) + assert dt == datetime.datetime(2013, 6, 30, 23, 59, 59) + + +def test_native_unpacking_incomplete(): + moment = temporenc.unpackb(temporenc.packb(type='DT', year=1983, hour=12)) + with pytest.raises(ValueError): + moment.date() + with pytest.raises(ValueError): + moment.time() + with pytest.raises(ValueError): + moment.datetime() + + moment = temporenc.unpackb(temporenc.packb(datetime.datetime.now().date())) + with pytest.raises(ValueError): + moment.datetime() + + +def test_native_time_zone(): + + # Python < 3.2 doesn't have concrete tzinfo implementations. This + # test uses the internal helper class instead to avoid depending on + # newer Python versions (or on pytz). + from temporenc.temporenc import FixedOffset + + dutch_winter = FixedOffset(60) # UTC +01:00 + zero_delta = datetime.timedelta(0) + hour_delta = datetime.timedelta(minutes=60) + + expected_name = "UTC+01:00" + assert dutch_winter.tzname(None) == expected_name + assert expected_name in str(dutch_winter) + assert expected_name in repr(dutch_winter) + assert dutch_winter.dst(None) == zero_delta + + # DTZ + actual = temporenc.packb( + datetime.datetime(1983, 1, 15, 18, 25, 12, 0, tzinfo=dutch_winter), + type='DTZ') + expected = from_hex('cf 7e 0e 93 26 44') + assert actual == expected + moment = temporenc.unpackb(expected) + assert moment.hour == 18 + assert moment.tz_offset == 60 + dt = moment.datetime() + assert dt.hour == 18 + assert dt.utcoffset() == hour_delta + + # DTSZ (microsecond, since native types have that precision) + actual = temporenc.packb( + datetime.datetime( + 1983, 1, 15, 18, 25, 12, 123456, + tzinfo=dutch_winter), + type='DTSZ') + dtsz_us = from_hex('eb df 83 a4 c9 83 c4 81 10') + assert actual == dtsz_us + moment = temporenc.unpackb(expected) + assert moment.datetime().hour == 18 + + # Time only with time zone + moment = temporenc.unpackb(temporenc.packb( + datetime.time(0, 30, 0, 123456, tzinfo=dutch_winter), + type='DTSZ')) + assert moment.tz_offset == 60 + for obj in [moment.time(), moment.datetime(strict=False)]: + assert (obj.hour, obj.minute, obj.microsecond) == (0, 30, 123456) + assert obj.utcoffset() == hour_delta + + +def test_string_conversion(): + + # Date only + value = temporenc.unpackb(temporenc.packb(year=1983, month=1, day=15)) + assert str(value) == "1983-01-15" + value = temporenc.unpackb(temporenc.packb(year=1983, day=15)) + assert str(value) == "1983-??-15" + + # Time only + value = temporenc.unpackb(temporenc.packb(hour=1, minute=2, second=3)) + assert str(value) == "01:02:03" + value = temporenc.unpackb(temporenc.packb( + hour=1, second=3, microsecond=12340)) + assert str(value) == "01:??:03.01234" + + # Date and time + value = temporenc.unpackb(temporenc.packb( + year=1983, month=1, day=15, + hour=18, minute=25)) + assert str(value) == "1983-01-15 18:25:??" + + # If sub-second is set but equal to 0, the string should show it + # properly anyway. + value = temporenc.unpackb(temporenc.packb( + hour=12, minute=34, second=56, microsecond=0)) + assert str(value) == "12:34:56.0" + + # Time zone info should be included + moment = temporenc.unpackb(from_hex('cf 7e 0e 93 26 40')) + assert str(moment) == '1983-01-15 18:25:12Z' + moment = temporenc.unpackb(from_hex('cf 7e 0e 93 26 44')) + assert str(moment) == '1983-01-15 18:25:12+01:00' + + # Very contrived example... + value = temporenc.unpackb(temporenc.packb(microsecond=1250)) + assert str(value) == "??:??:??.00125" + + # And a basic one for repr() + value = temporenc.unpackb(temporenc.packb(hour=12, minute=34, second=56)) + assert '12:34:56' in repr(value) + + +def test_comparison(): + now = datetime.datetime.now() + later = now.replace(microsecond=0) + datetime.timedelta(hours=1) + v1 = temporenc.unpackb(temporenc.packb(now)) + v2 = temporenc.unpackb(temporenc.packb(now)) + v3 = temporenc.unpackb(temporenc.packb(later)) + + # Same + assert v1 == v2 + assert v1 != v3 + assert v1 >= v2 + assert v1 >= v2 + assert not (v1 > v2) + assert not (v1 < v2) + + # Different + assert v3 > v1 + assert v1 < v3 + assert v3 >= v1 + assert v1 <= v3 + + # Equality tests against other types: not equal + bogus = 'junk' + assert not (v1 == bogus) + assert v1 != bogus + + # Comparison against other types: + # * fail on Python 3 (unorderable types) + # * use fallback comparison on Python 2. + for op in (operator.gt, operator.lt, operator.ge, operator.le): + if PY2: + op(v1, bogus) # should not raise + else: + with pytest.raises(TypeError): + op(v1, bogus) # should raise + + +def test_hash(): + + now = datetime.datetime.now() + later = now.replace(microsecond=0) + datetime.timedelta(hours=1) + v1 = temporenc.unpackb(temporenc.packb(now)) + v2 = temporenc.unpackb(temporenc.packb(now)) + v3 = temporenc.unpackb(temporenc.packb(later)) + + assert hash(v1) == hash(v2) + assert hash(v1) != hash(v3) + + d = {} + d[v1] = 1 + d[v2] = 2 + d[v3] = 3 + assert len(d) == 2 + assert d[v1] == 2 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..bd7c6d1a24 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py27,py33,py34,py35,py36 + +[testenv] +deps=-rrequirements-test.txt +commands=py.test tests/ From 9432e5178eadd3eb1c99abdb9fb36d1150cf70ff Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 14:47:50 +0900 Subject: [PATCH 5/7] setup: Add the precompiled wheel of temporenc --- .gitignore | 4 +++- python.lock | 5 +++++ vendor/wheelhouse/temporenc-0.1.0-py3-none-any.whl | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 vendor/wheelhouse/temporenc-0.1.0-py3-none-any.whl diff --git a/.gitignore b/.gitignore index c0931429bb..f88942ecc1 100644 --- a/.gitignore +++ b/.gitignore @@ -132,4 +132,6 @@ ENV/ .python-runtime -docs/manager/rest-reference/openapi.json \ No newline at end of file +docs/manager/rest-reference/openapi.json +/vendor/*/build/ +/vendor/*/dist/ diff --git a/python.lock b/python.lock index 2004c39b8e..e6c53ec80b 100644 --- a/python.lock +++ b/python.lock @@ -3668,6 +3668,11 @@ }, { "artifacts": [ + { + "algorithm": "sha256", + "hash": "74276ab743a145305cc28e86212f45a30ad0fc5dbe39122e5ecadba04a8def65", + "url": "file://${WHEELS_DIR}/temporenc-0.1.0-py3-none-any.whl" + }, { "algorithm": "sha256", "hash": "b07e8ea0684e73ded0cd4fa3622ca00477ee85cf32ea686f38db06c2e8e17bda", diff --git a/vendor/wheelhouse/temporenc-0.1.0-py3-none-any.whl b/vendor/wheelhouse/temporenc-0.1.0-py3-none-any.whl new file mode 100644 index 0000000000..0d80378d63 --- /dev/null +++ b/vendor/wheelhouse/temporenc-0.1.0-py3-none-any.whl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74276ab743a145305cc28e86212f45a30ad0fc5dbe39122e5ecadba04a8def65 +size 10280 From eec0cedbf3026baa47827b93def59f2773190d6b Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 14:52:56 +0900 Subject: [PATCH 6/7] doc: Add news fragment --- changes/1370.deps.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/1370.deps.md diff --git a/changes/1370.deps.md b/changes/1370.deps.md new file mode 100644 index 0000000000..1956a10a77 --- /dev/null +++ b/changes/1370.deps.md @@ -0,0 +1 @@ +Vendor `asyncudp` and `temporenc` into the `vendor/` directory with precompiled wheels to avoid packaging issues From e813f412ebe8a676b15bff3412017d2563d5da5f Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Mon, 3 Jul 2023 15:00:05 +0900 Subject: [PATCH 7/7] ci: Introduce constraints.txt to pin Rx version --- pants.toml | 3 +++ python.lock | 55 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/pants.toml b/pants.toml index cb0ff353d8..aba6208c1b 100644 --- a/pants.toml +++ b/pants.toml @@ -62,6 +62,9 @@ indexes = ["https://dist.backend.ai/pypi/simple/", "https://pypi.org/simple/"] find_links = ["file://%(buildroot)s/vendor/wheelhouse"] path_mappings = ["WHEELS_DIR|%(buildroot)s/vendor/wheelhouse"] +[python.resolves_to_constraints_file] +python-default = "constraints.txt" + [python.resolves] python-default = "python.lock" python-kernel = "python-kernel.lock" diff --git a/python.lock b/python.lock index e6c53ec80b..9ec37c5902 100644 --- a/python.lock +++ b/python.lock @@ -99,7 +99,9 @@ // "zipstream-new~=1.1.8" // ], // "manylinux": "manylinux2014", -// "requirement_constraints": [], +// "requirement_constraints": [ +// "Rx~=3.2.0" +// ], // "only_binary": [], // "no_binary": [] // } @@ -110,7 +112,9 @@ "allow_prereleases": false, "allow_wheels": true, "build_isolation": true, - "constraints": [], + "constraints": [ + "Rx~=3.2.0" + ], "locked_resolves": [ { "locked_requirements": [ @@ -1540,34 +1544,32 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad", - "url": "https://files.pythonhosted.org/packages/11/71/d51beba3d8986fa6d8670ec7bcba989ad6e852d5ae99d95633e5dacc53e7/graphql_core-2.3.2-py2.py3-none-any.whl" + "hash": "6288fe97c32d2f868a2dfe62e766dc85d48c96c1d085294edf44714190f2e4f3", + "url": "https://files.pythonhosted.org/packages/f1/88/a4a7bf8ab66c35b146e44d77a1f9fd2c36e0ec9fb1a51581608c16deb6e3/graphql_core-2.2-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "aac46a9ac524c9855910c14c48fc5d60474def7f99fd10245e76608eba7af746", - "url": "https://files.pythonhosted.org/packages/88/a2/dd91d55a6f6dd88c4d3c284d387c94f1f933fedec43a86a4422940b9de18/graphql-core-2.3.2.tar.gz" + "hash": "60ef8277b82aaad49e87154a0288a9542a82a63909568375712f826b1c280ef5", + "url": "https://files.pythonhosted.org/packages/ee/69/4a9345f33e117129f68d8eca601f0c37057266f5df5b3be96442e1df91b2/graphql-core-2.2.tar.gz" } ], "project_name": "graphql-core", "requires_dists": [ - "coveralls==1.11.1; extra == \"test\"", - "cython==0.29.17; extra == \"test\"", - "gevent==1.5.0; extra == \"test\"", + "coveralls; extra == \"test\"", "gevent>=1.1; extra == \"gevent\"", - "promise<3,>=2.3", - "pyannotate==1.2.0; extra == \"test\"", - "pytest-benchmark==3.2.3; extra == \"test\"", - "pytest-cov==2.8.1; extra == \"test\"", - "pytest-django==3.9.0; extra == \"test\"", - "pytest-mock==2.0.0; extra == \"test\"", - "pytest==4.6.10; extra == \"test\"", - "rx<2,>=1.6", - "six==1.14.0; extra == \"test\"", - "six>=1.10.0" + "gevent>=1.1; extra == \"test\"", + "promise>=2.1", + "pytest-benchmark==3.0.0; extra == \"test\"", + "pytest-cov==2.3.1; extra == \"test\"", + "pytest-django==2.9.1; extra == \"test\"", + "pytest-mock==1.2; extra == \"test\"", + "pytest<4.0,>=3.3; extra == \"test\"", + "rx>=1.6.0", + "six>=1.10.0", + "six>=1.10.0; extra == \"test\"" ], "requires_python": null, - "version": "2.3.2" + "version": "2.2" }, { "artifacts": [ @@ -3393,14 +3395,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "ca71b65d0fc0603a3b5cfaa9e33f5ba81e4aae10a58491133595088d7734b2da", - "url": "https://files.pythonhosted.org/packages/3c/51/d37235bad8df7536cc950e0d0a26e94131a6a3f7d5e1bed5f37f0846f2ef/Rx-1.6.3.tar.gz" + "hash": "922c5f4edb3aa1beaa47bf61d65d5380011ff6adcd527f26377d05cb73ed8ec8", + "url": "https://files.pythonhosted.org/packages/e2/a9/efeaeca4928a9a56d04d609b5730994d610c82cf4d9dd7aa173e6ef4233e/Rx-3.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "b657ca2b45aa485da2f7dcfd09fac2e554f7ac51ff3c2f8f2ff962ecd963d91c", + "url": "https://files.pythonhosted.org/packages/34/b5/e0f602453b64b0a639d56f3c05ab27202a4eec993eb64d66c077c821b621/Rx-3.2.0.tar.gz" } ], "project_name": "rx", "requires_dists": [], - "requires_python": null, - "version": "1.6.3" + "requires_python": ">=3.6.0", + "version": "3.2.0" }, { "artifacts": [