├── .gitignore ├── .py_vir_env ├── .readthedocs.yaml ├── CHANGES.rst ├── LICENSE ├── README.rst ├── clean_build.sh ├── docs ├── Makefile ├── conf.py └── index.rst ├── load_tests.py ├── pyflakes.sh ├── requirements.txt ├── runtests.sh ├── setup.py ├── tox.ini ├── unittests └── test_when.py └── when.py /.gitignore: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | .DS_Store 8 | *.pyc 9 | *.o 10 | *.so 11 | *.swp 12 | *~ 13 | .coverage* 14 | htmlcov/ 15 | foo.py 16 | debug.log 17 | db.sqlite3 18 | logs/* 19 | *.egg 20 | *.egg-info 21 | build/ 22 | dist/ 23 | docs/_build/ 24 | extras/sample_site/uploads/ 25 | .tox/ 26 | .eggs/ 27 | -------------------------------------------------------------------------------- /.py_vir_env: -------------------------------------------------------------------------------- 1 | WHN 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: requirements.txt 23 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.11.2 2 | ====== 3 | 4 | * 2023/06/29 5 | * Modified test targets: 3.8-3.11 6 | 7 | 8 | 0.11.1 9 | ====== 10 | 11 | * Added Py 3.10 testing 12 | 13 | 14 | 0.11 15 | ==== 16 | 17 | * Added Py 3.7, 3.8, 3.9 testing 18 | * dropped Py2.7, Py3.5 testing 19 | 20 | 21 | 0.10 22 | ==== 23 | 24 | * project was initially part of http://python-wrench.readthedocs.io/en/latest/ 25 | * initial pypi commit of indendent library 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christopher Trudeau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | when 2 | **** 3 | 4 | A wrapper library for date/time conversion. Takes many of the standard ISO 5 | date formats as strings and creates an internally consistent datetime object. 6 | 7 | Installation 8 | ============ 9 | 10 | .. code-block:: bash 11 | 12 | $ pip install pywhen 13 | 14 | Supports 15 | ======== 16 | 17 | pywhen has been tested with Python 3.8-3.11 18 | 19 | Previously it was tested with Python 2.7, 3.6-3.7. Not much has changed since 20 | then and it should still work, but it isn't part of the automatic tests 21 | anymore. 22 | 23 | Docs & Source 24 | ============= 25 | 26 | Docs: http://pywhen.readthedocs.io/en/latest/ 27 | 28 | Source: https://github.com/cltrudeau/pywhen 29 | -------------------------------------------------------------------------------- /clean_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=`grep "__version__ = " when.py | cut -d "'" -f 2` 4 | 5 | git tag "$version" 6 | 7 | if [ "$?" != "0" ] ; then 8 | exit $? 9 | fi 10 | 11 | rm -rf build 12 | rm -rf dist 13 | python setup.py sdist 14 | python setup.py bdist_wheel --universal 15 | 16 | echo "------------------------" 17 | echo 18 | echo "now do:" 19 | echo " twine upload dist/*" 20 | echo 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flowr.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flowr.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flowr" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flowr" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Customized Sphinx config file. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # All configuration values have a default; values that are commented out 10 | # serve to show the default. 11 | 12 | # =========================================================================== 13 | # Project Customization 14 | # -- change these variables for the project 15 | 16 | project = 'when' 17 | project_capitalized = project.capitalize() 18 | copyright_start = 2015 19 | copyright_owner = 'Christopher Trudeau' 20 | 21 | # short version number (X.Y) 22 | import imp 23 | mod = imp.load_source(project, '../when.py') 24 | version = mod.__version__ 25 | 26 | # =========================================================================== 27 | # Sphinx Customization Starts 28 | # - you probably don't need to touch anything after this point 29 | # =========================================================================== 30 | 31 | import sys 32 | import os 33 | import shlex 34 | 35 | # If extensions (or modules to document with autodoc) are in another directory, 36 | # add these directories to sys.path here. If the directory is relative to the 37 | # documentation root, use os.path.abspath to make it absolute, like shown here. 38 | sys.path.insert(0, os.path.abspath('..')) 39 | 40 | # -- General configuration ------------------------------------------------ 41 | 42 | # If your documentation needs a minimal Sphinx version, state it here. 43 | #needs_sphinx = '1.0' 44 | 45 | # Add any Sphinx extension module names here, as strings. They can be 46 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 47 | # ones. 48 | extensions = [ 49 | 'sphinx.ext.autodoc', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # source_suffix = ['.rst', '.md'] 58 | source_suffix = '.rst' 59 | 60 | # The encoding of source files. 61 | #source_encoding = 'utf-8-sig' 62 | 63 | # The master toctree document. 64 | master_doc = 'index' 65 | 66 | # General information about the project. 67 | import datetime 68 | year = datetime.datetime.now().year 69 | if year == copyright_start: 70 | copyright = '%d, %s' % (year, copyright_owner) 71 | else: 72 | copyright = '%d-%d, %s' % (copyright_start, year, copyright_owner) 73 | 74 | #version = mod.VERSION 75 | #version = mod.SETUP_ARGS['version'] 76 | # The full version, including alpha/beta/rc tags. 77 | release = version 78 | 79 | # The language for content autogenerated by Sphinx. Refer to documentation 80 | # for a list of supported languages. 81 | # 82 | # This is also used if you do content translation via gettext catalogs. 83 | # Usually you set "language" from the command line for these cases. 84 | language = None 85 | 86 | # There are two options for replacing |today|: either, you set today to some 87 | # non-false value, then it is used: 88 | #today = '' 89 | # Else, today_fmt is used as the format for a strftime call. 90 | #today_fmt = '%B %d, %Y' 91 | 92 | # List of patterns, relative to source directory, that match files and 93 | # directories to ignore when looking for source files. 94 | exclude_patterns = ['_build'] 95 | 96 | # The reST default role (used for this markup: `text`) to use for all 97 | # documents. 98 | #default_role = None 99 | 100 | # If true, '()' will be appended to :func: etc. cross-reference text. 101 | #add_function_parentheses = True 102 | 103 | # If true, the current module name will be prepended to all description 104 | # unit titles (such as .. function::). 105 | #add_module_names = True 106 | 107 | # If true, sectionauthor and moduleauthor directives will be shown in the 108 | # output. They are ignored by default. 109 | #show_authors = False 110 | 111 | # The name of the Pygments (syntax highlighting) style to use. 112 | pygments_style = 'sphinx' 113 | 114 | # A list of ignored prefixes for module index sorting. 115 | #modindex_common_prefix = [] 116 | 117 | # If true, keep warnings as "system message" paragraphs in the built documents. 118 | #keep_warnings = False 119 | 120 | # If true, `todo` and `todoList` produce output, else they produce nothing. 121 | todo_include_todos = False 122 | 123 | 124 | # -- Options for HTML output ---------------------------------------------- 125 | 126 | # The theme to use for HTML and HTML Help pages. See the documentation for 127 | # a list of builtin themes. 128 | html_theme = 'sphinx_rtd_theme' 129 | 130 | # Theme options are theme-specific and customize the look and feel of a theme 131 | # further. For a list of options available for each theme, see the 132 | # documentation. 133 | #html_theme_options = {} 134 | 135 | # Add any paths that contain custom themes here, relative to this directory. 136 | #html_theme_path = [] 137 | 138 | # The name for this set of Sphinx documents. If None, it defaults to 139 | # " v documentation". 140 | #html_title = None 141 | 142 | # A shorter title for the navigation bar. Default is the same as html_title. 143 | #html_short_title = None 144 | 145 | # The name of an image file (relative to this directory) to place at the top 146 | # of the sidebar. 147 | #html_logo = None 148 | 149 | # The name of an image file (within the static path) to use as favicon of the 150 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 151 | # pixels large. 152 | #html_favicon = None 153 | 154 | # Add any paths that contain custom static files (such as style sheets) here, 155 | # relative to this directory. They are copied after the builtin static files, 156 | # so a file named "default.css" will overwrite the builtin "default.css". 157 | #html_static_path = ['_static'] 158 | 159 | # Add any extra paths that contain custom files (such as robots.txt or 160 | # .htaccess) here, relative to this directory. These files are copied 161 | # directly to the root of the documentation. 162 | #html_extra_path = [] 163 | 164 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 165 | # using the given strftime format. 166 | #html_last_updated_fmt = '%b %d, %Y' 167 | 168 | # If true, SmartyPants will be used to convert quotes and dashes to 169 | # typographically correct entities. 170 | #html_use_smartypants = True 171 | 172 | # Custom sidebar templates, maps document names to template names. 173 | #html_sidebars = {} 174 | 175 | # Additional templates that should be rendered to pages, maps page names to 176 | # template names. 177 | #html_additional_pages = {} 178 | 179 | # If false, no module index is generated. 180 | #html_domain_indices = True 181 | 182 | # If false, no index is generated. 183 | #html_use_index = True 184 | 185 | # If true, the index is split into individual pages for each letter. 186 | #html_split_index = False 187 | 188 | # If true, links to the reST sources are added to the pages. 189 | #html_show_sourcelink = True 190 | 191 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 192 | #html_show_sphinx = True 193 | 194 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 195 | #html_show_copyright = True 196 | 197 | # If true, an OpenSearch description file will be output, and all pages will 198 | # contain a tag referring to it. The value of this option must be the 199 | # base URL from which the finished HTML is served. 200 | #html_use_opensearch = '' 201 | 202 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 203 | #html_file_suffix = None 204 | 205 | # Language to be used for generating the HTML full-text search index. 206 | # Sphinx supports the following languages: 207 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 208 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 209 | #html_search_language = 'en' 210 | 211 | # A dictionary with options for the search language support, empty by default. 212 | # Now only 'ja' uses this config value 213 | #html_search_options = {'type': 'default'} 214 | 215 | # The name of a javascript file (relative to the configuration directory) that 216 | # implements a search results scorer. If empty, the default will be used. 217 | #html_search_scorer = 'scorer.js' 218 | 219 | # Output file base name for HTML help builder. 220 | htmlhelp_basename = '%sdoc' % project_capitalized 221 | 222 | # -------------------------------------------------------------------- 223 | # Custom 224 | # - fix the default behaviour of skipping of __init__ docstrings 225 | 226 | def skip(app, what, name, obj, skip, options): 227 | if name == '__init__': 228 | return False 229 | 230 | return skip 231 | 232 | def setup(app): 233 | app.connect('autodoc-skip-member', skip) 234 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Version: |version| 4 | 5 | Methods 6 | ======= 7 | 8 | .. automodule:: when 9 | :members: 10 | 11 | Indices and tables 12 | ================== 13 | 14 | * :ref:`genindex` 15 | * :ref:`modindex` 16 | * :ref:`search` 17 | -------------------------------------------------------------------------------- /load_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest, sys 3 | from waelstow import discover_tests, list_tests 4 | 5 | def get_suite(labels=[]): 6 | return discover_tests('unittests', labels) 7 | 8 | 9 | if __name__ == '__main__': 10 | suite = get_suite(sys.argv[1:]) 11 | unittest.TextTestRunner(verbosity=1).run(suite) 12 | -------------------------------------------------------------------------------- /pyflakes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "============================================================" 4 | echo "== pyflakes ==" 5 | pyflakes when.py unittests 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # These requirements are for running tests and the sample_site, the library 2 | # itself only needs a subset, see setup.py for that list 3 | coverage==7.2.7 4 | pudb==2022.1.3 5 | pyflakes==3.0.1 6 | Sphinx==6.2.1 7 | sphinx-rtd-theme==1.2.2 8 | tox==4.6.3 9 | twine==4.0.2 10 | waelstow==0.11.1 11 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | find . -name "*.pyc" -exec rm {} \; 4 | coverage run -p --source=unittests,when ./load_tests.py $@ 5 | if [ "$?" = "0" ]; then 6 | coverage combine 7 | echo -e "\n\n================================================" 8 | echo "Test Coverage" 9 | coverage report 10 | echo -e "\nrun \"coverage html\" for full report" 11 | echo -e "\n" 12 | ./pyflakes.sh 13 | fi 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | from when import __version__ 4 | 5 | readme = os.path.join(os.path.dirname(__file__), 'README.rst') 6 | long_description = open(readme).read() 7 | 8 | SETUP_ARGS = dict( 9 | name='pywhen', 10 | version=__version__, 11 | description=('Collection of random python tools and utilities '), 12 | long_description=long_description, 13 | url='https://github.com/cltrudeau/wrench', 14 | author='Christopher Trudeau', 15 | author_email='ctrudeau+pypi@arsensa.com', 16 | license='MIT', 17 | include_package_data=True, 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Environment :: Web Environment', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python :: 3.8', 25 | 'Programming Language :: Python :: 3.9', 26 | 'Programming Language :: Python :: 3.10', 27 | 'Programming Language :: Python :: 3.11', 28 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 29 | 'Topic :: Software Development :: Libraries :: Python Modules', 30 | ], 31 | keywords='tools', 32 | test_suite='load_tests.get_suite', 33 | py_modules = ['when',], 34 | install_requires=[], 35 | tests_require=[ 36 | 'waelstow==0.10.2', 37 | ], 38 | ) 39 | 40 | if __name__ == '__main__': 41 | from setuptools import setup 42 | setup(**SETUP_ARGS) 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39,310,311} 3 | 4 | [testenv] 5 | commands= 6 | python setup.py test 7 | -------------------------------------------------------------------------------- /unittests/test_when.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, date, time 2 | from unittest import TestCase 3 | 4 | from when import When 5 | 6 | # ============================================================================= 7 | 8 | class TestWhen(TestCase): 9 | def setUp(self): 10 | self.full_date = datetime(1972, 1, 31, 13, 55) 11 | self.zero_date = datetime(1972, 1, 31, 0, 0) 12 | self.only_time = time(13, 55) 13 | self.epoch = 65732100 14 | self.mepoch = self.epoch * 1000 15 | self.micro_epoch = '%s.0000' % (self.epoch) 16 | 17 | def test_constructor(self): 18 | self.assertEqual(self.full_date, When(datetime=self.full_date).datetime) 19 | self.assertEqual(self.full_date, 20 | When(date=date(1972, 1, 31), time=time(13, 55)).datetime) 21 | self.assertEqual(self.full_date, 22 | When(date=date(1972, 1, 31), time_string='13:55').datetime) 23 | self.assertEqual(self.full_date, 24 | When(date=date(1972, 1, 31), time_string='13:55:00').datetime) 25 | 26 | self.assertEqual(self.full_date, When(epoch=self.epoch).datetime) 27 | self.assertEqual(self.full_date, When(milli_epoch=self.mepoch).datetime) 28 | 29 | # default time 30 | self.assertEqual(self.zero_date, When(date=date(1972, 1, 31)).datetime) 31 | 32 | # time only 33 | self.assertEqual(self.only_time, When(time=time(13, 55)).time) 34 | self.assertEqual(self.only_time, When(time_string='13:55').time) 35 | 36 | # parse detection 37 | self.assertEqual(self.zero_date, When(detect='1972-01-31').datetime) 38 | self.assertEqual(self.only_time, When(detect='13:55').time) 39 | self.assertEqual(self.only_time, When(detect='13:55:00').time) 40 | self.assertEqual(self.full_date, 41 | When(detect='1972-01-31 13:55').datetime) 42 | self.assertEqual(self.full_date, 43 | When(detect='1972-01-31 13:55:00').datetime) 44 | self.assertEqual(self.full_date, 45 | When(detect='1972-01-31T13:55Z').datetime) 46 | self.assertEqual(self.full_date, 47 | When(detect='1972-01-31T13:55:00Z').datetime) 48 | self.assertEqual(self.full_date, 49 | When(detect='1972-01-31T13:55:00.00Z').datetime) 50 | 51 | # parse force 52 | self.assertEqual(self.zero_date, When(parse_date='1972-01-31').datetime) 53 | self.assertEqual(self.only_time, When(parse_time='13:55').time) 54 | self.assertEqual(self.only_time, When(parse_time_sec='13:55:00').time) 55 | self.assertEqual(self.full_date, 56 | When(parse_datetime='1972-01-31 13:55').datetime) 57 | self.assertEqual(self.full_date, 58 | When(parse_datetime_sec='1972-01-31 13:55:00').datetime) 59 | self.assertEqual(self.full_date, 60 | When(parse_datetime_utc='1972-01-31T13:55Z').datetime) 61 | self.assertEqual(self.full_date, 62 | When(parse_datetime_sec_utc='1972-01-31T13:55:00Z').datetime) 63 | self.assertEqual(self.full_date, 64 | When(parse_iso_micro='1972-01-31T13:55:00.00Z').datetime) 65 | 66 | # check errors 67 | with self.assertRaises(ValueError): 68 | When(detect='abc') 69 | 70 | with self.assertRaises(ValueError): 71 | When(parse_date='abc') 72 | 73 | with self.assertRaises(ValueError): 74 | When(parse_time='abc') 75 | 76 | with self.assertRaises(ValueError): 77 | When(parse_time_sec='abc') 78 | 79 | with self.assertRaises(ValueError): 80 | When(parse_datetime='abc') 81 | 82 | with self.assertRaises(ValueError): 83 | When(parse_datetime_sec='abc') 84 | 85 | with self.assertRaises(ValueError): 86 | When(parse_datetime_utc='abc') 87 | 88 | with self.assertRaises(ValueError): 89 | When(parse_datetime_sec_utc='abc') 90 | 91 | with self.assertRaises(ValueError): 92 | When(parse_iso_micro='abc') 93 | 94 | with self.assertRaises(AttributeError): 95 | When() 96 | 97 | def test_values(self): 98 | when = When(datetime=self.full_date) 99 | 100 | self.assertEqual('1972-01-31', when.string.date) 101 | self.assertEqual('13:55', when.string.time) 102 | self.assertEqual('13:55:00', when.string.time_sec) 103 | self.assertEqual('1972-01-31 13:55', when.string.datetime) 104 | self.assertEqual('1972-01-31 13:55:00', when.string.datetime_sec) 105 | self.assertEqual('1972-01-31T13:55Z', when.string.datetime_utc) 106 | self.assertEqual('1972-01-31T13:55:00Z', when.string.datetime_sec_utc) 107 | self.assertEqual('1972-01-31T13:55:00.0Z', when.string.iso_micro) 108 | 109 | self.assertEqual(self.full_date, when.datetime) 110 | self.assertEqual(self.full_date.date(), when.date) 111 | self.assertEqual(self.only_time, when.time) 112 | self.assertEqual(self.epoch, when.epoch) 113 | self.assertEqual(self.mepoch, when.milli_epoch) 114 | -------------------------------------------------------------------------------- /when.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.11.2' 2 | 3 | import sys 4 | from datetime import datetime, time 5 | import time as time_mod 6 | 7 | # ============================================================================= 8 | 9 | # compensate for long disappearing in py3 10 | if sys.version_info > (3,): 11 | long = int 12 | 13 | # ============================================================================= 14 | # Date Conversion 15 | # ============================================================================= 16 | 17 | class TimeOnlyError(Exception): 18 | """Exception indicating that a date operation was attempted on a 19 | :class:`When` object that only wraps a python ``time`` instance.""" 20 | pass 21 | 22 | 23 | class When(object): 24 | # https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior 25 | """Date/time conversion utility. A When object wraps either a python 26 | ``datetime`` or ``time`` instance, providing a common mechanism for 27 | building and converting them into different formats. 28 | 29 | If When is wrapping a ``time`` object then some methods will raise a 30 | :class:`TimeOnlyError`. 31 | 32 | .. _when-formats: 33 | 34 | Supported formats for parsing and display:: 35 | 36 | 'date': '%Y-%m-%d' 37 | 'time': '%H:%M' 38 | 'time_sec': '%H:%M:%S' 39 | 'datetime': '%Y-%m-%d %H:%M' 40 | 'datetime_sec': '%Y-%m-%d %H:%M:%S' 41 | 'datetime_utc': '%Y-%m-%dT%H:%MZ' 42 | 'datetime_sec_utc': '%Y-%m-%dT%H:%M:%SZ' 43 | 'iso_micro': '%Y-%m-%dT%H:%M:%S.%fZ' 44 | 45 | .. warning:: 46 | 47 | All python ``datetime`` objects in this class are naive and have no 48 | timezone information associated with them, this includes various 49 | formats labelled "utc". 50 | """ 51 | parse_formats = { 52 | 'date': '%Y-%m-%d', 53 | 'time': '%H:%M', 54 | 'time_sec': '%H:%M:%S', 55 | 'datetime': '%Y-%m-%d %H:%M', 56 | 'datetime_sec': '%Y-%m-%d %H:%M:%S', 57 | 'datetime_utc': '%Y-%m-%dT%H:%MZ', 58 | 'datetime_sec_utc': '%Y-%m-%dT%H:%M:%SZ', 59 | 'iso_micro': '%Y-%m-%dT%H:%M:%S.%fZ', 60 | } 61 | 62 | class WhenStrformat(object): 63 | def __init__(self, when): 64 | self.when = when 65 | 66 | def __getattribute__(self, name): 67 | if name == 'when': 68 | return object.__getattribute__(self, name) 69 | 70 | formatter = self.when.parse_formats[name] 71 | if name == 'iso_micro': 72 | # iso_micro needs special handling as python's %f returns a 73 | # zero padded number but the standard doesn't pad the value 74 | formatter = self.when.parse_formats['datetime_sec_utc'] 75 | 76 | result = self.when.datetime.strftime(formatter) 77 | 78 | if name == 'iso_micro': 79 | result = result[:-1] + '.%dZ' % int( 80 | self.when.datetime.strftime('%f')) 81 | 82 | return result 83 | 84 | 85 | def __init__(self, **kwargs): 86 | """Create a When object. The constructor accepts a variety of 87 | keywords depending on what type of date or time information you wish 88 | to convert. Most keywords result in a python ``datetime`` object 89 | being wrapped, but certain cases use a ``time`` object. If the When 90 | is only a ``time`` wrapper then some of the methods will not be 91 | available. 92 | 93 | :param datetime: 94 | Create ``When`` using a python ``datetime`` object 95 | :param date: 96 | Create ``When`` using a python ``date`` object, can be used in 97 | conjunction with the ``time`` keyword. If used without the 98 | ``time`` keyword the time portion will be set to midnight. 99 | :param time: 100 | Create ``When`` using a python ``time`` object. If used on its 101 | own the date based methods of ``When`` will not be allowed. This 102 | keyword can be used in conjunction with the ``date`` keyword to 103 | create a fully qualified ``When``. 104 | :param time_string: 105 | Create ``When`` using a string containing time information. 106 | Handles either 'hour:minute' or 'hour:minute:second'. Like the 107 | ``time`` keyword, can be used in conjunction with ``date``. 108 | :param epoch: 109 | Create ``When`` using an integer epoch value 110 | :param milli_epoch: 111 | Create ``When`` using an integer that is 1000 * an epoch value with 112 | the last thousands being milli-epoch. 113 | :param detect: 114 | Create ``When`` by parsing a string which is compared against the 115 | list of available string parsers. 116 | :param parse_*: 117 | Create ``When`` by parsing a string using the specific 118 | :ref:`Supported formats ` given. 119 | For example, ``parse_iso_micro`` expects the ISO 8601 format. 120 | 121 | :raises ValueError: 122 | If a bad string is passed to ``detect`` or ``parse_*`` keywords 123 | :raises AttributeError: 124 | If the constructor was called without sufficient arguments to 125 | result in a date or time being wrapped. 126 | """ 127 | self._datetime = None 128 | self._time = None 129 | 130 | if 'datetime' in kwargs: 131 | self._datetime = kwargs['datetime'] 132 | elif 'date' in kwargs: 133 | d = kwargs['date'] 134 | if 'time' in kwargs: 135 | t = kwargs['time'] 136 | elif 'time_string' in kwargs: 137 | t = self._parse_time_string(kwargs['time_string']) 138 | else: 139 | t = time() 140 | 141 | self._datetime = datetime(d.year, d.month, d.day, t.hour, t.minute, 142 | t.second) 143 | elif 'time' in kwargs: 144 | self._time = kwargs['time'] 145 | elif 'time_string' in kwargs: 146 | self._time = self._parse_time_string(kwargs['time_string']) 147 | elif 'epoch' in kwargs: 148 | self._datetime = datetime.fromtimestamp(kwargs['epoch']) 149 | elif 'milli_epoch' in kwargs: 150 | mepoch = kwargs['milli_epoch'] 151 | epoch = float(mepoch) / 1000.0 152 | milli = float(mepoch) - (epoch * 1000.0) 153 | self._datetime = datetime.fromtimestamp(long(epoch)) 154 | self._datetime.replace(microsecond = int(milli / 10.0)) 155 | elif 'detect' in kwargs: 156 | for f in self.parse_formats.values(): 157 | try: 158 | self._datetime = datetime.strptime(kwargs['detect'], f) 159 | break 160 | except ValueError: 161 | # couldn't parse using this format, ignore and try again 162 | pass 163 | 164 | if not self._datetime: 165 | # nothing parsed 166 | raise ValueError(('could not parse the date/time passed to ' 167 | 'detect')) 168 | else: 169 | # loop through all the possible kwargs looking for parse_* keys, 170 | # if found parse based on that and stop 171 | for key, value in kwargs.items(): 172 | if key.startswith('parse_'): 173 | f = self.parse_formats[key[6:]] 174 | if key.startswith('parse_time'): 175 | self._time = self._parse_time_string(value) 176 | else: 177 | self._datetime = datetime.strptime(value, f) 178 | 179 | break 180 | 181 | if not self._datetime and not self._time: 182 | raise AttributeError('invalid keyword arguments') 183 | 184 | def _parse_time_string(self, value): 185 | parts = value.split(':') 186 | parts = [int(p) for p in parts] 187 | return time(*parts) 188 | 189 | @property 190 | def string(self): 191 | """Returns a placeholder object that has an attribute for each one of 192 | the :ref:`Supported formats `. 193 | 194 | Example:: 195 | 196 | >>> When(datetime=d).string.iso_micro 197 | 1972-01-31T13:45:00.2 198 | 199 | """ 200 | return When.WhenStrformat(self) 201 | 202 | @property 203 | def date(self): 204 | """Returns a python ``date`` object.""" 205 | return self._datetime.date() 206 | 207 | @property 208 | def datetime(self): 209 | """Returns a python ``datetime`` object.""" 210 | return self._datetime 211 | 212 | @property 213 | def time(self): 214 | """Returns a python ``time`` object.""" 215 | if self._datetime: 216 | return self._datetime.time() 217 | 218 | return self._time 219 | 220 | @property 221 | def epoch(self): 222 | """Returns an integer version of epoch, i.e. the number of seconds 223 | since Jan 1, 1970.""" 224 | return long(time_mod.mktime(self._datetime.timetuple())) 225 | 226 | @property 227 | def milli_epoch(self): 228 | """Returns an int of the epoch * 1000 + milliseconds.""" 229 | return self.epoch * 1000 + self._datetime.microsecond * 10 230 | --------------------------------------------------------------------------------