├── CONTRIBUTING.rst ├── MANIFEST.in ├── jinja2_time ├── __init__.py └── jinja2_time.py ├── setup.cfg ├── tox.ini ├── Makefile ├── CONTRIBUTORS.rst ├── HISTORY.rst ├── .github └── workflows │ └── run-checks.yml ├── .gitignore ├── LICENSE ├── setup.py ├── tests └── test_now.py └── README.rst /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | -------------------------------------------------------------------------------- /jinja2_time/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .jinja2_time import TimeExtension 4 | 5 | __author__ = 'Raphael Pierzina' 6 | __email__ = 'raphael@hackebrot.de' 7 | __version__ = '0.2.0' 8 | 9 | 10 | __all__ = ['TimeExtension'] 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:setup.py] 8 | 9 | [bumpversion:file:jinja2_time/__init__.py] 10 | 11 | [wheel] 12 | universal = 1 13 | 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35,pypy,flake8 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | freezegun 8 | commands = py.test {posargs:tests} 9 | 10 | [testenv:flake8] 11 | deps = flake8 12 | commands = flake8 jinja2_time setup.py tests 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-tox clean-build clean-py 2 | 3 | help: 4 | @echo "clean-build - remove build artifacts" 5 | @echo "clean-py - remove Python file artifacts" 6 | @echo "clean-tox - remove tox file artifacts" 7 | @echo "clean - remove all file artifacts" 8 | 9 | clean: clean-tox clean-build clean-py 10 | 11 | clean-tox: 12 | rm -rf .tox/ 13 | 14 | clean-build: 15 | rm -rf build/ 16 | rm -rf dist/ 17 | rm -rf *.egg-info 18 | 19 | clean-py: 20 | find . -type f -name "*.py[co]" -delete 21 | find . -type d -name "__pycache__" -delete 22 | 23 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Raphael Pierzina (`@hackebrot`_) 9 | 10 | Pull Requests and Patches 11 | ------------------------- 12 | 13 | * Juan Madurga (`@jlmadurga`_) 14 | * Fábio C. Barrionuevo da Luz (`@luzfcb`_) 15 | 16 | Bug Reports and Suggestions 17 | --------------------------- 18 | 19 | None yet. Why not be the first? 20 | 21 | .. _`@hackebrot`: https://github.com/hackebrot 22 | .. _`@jlmadurga`: https://github.com/jlmadurga 23 | .. _`@luzfcb`: https://github.com/luzfcb 24 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.3.0 (????-??-??) 7 | ------------------ 8 | 9 | * Limit arrow version to 0.13.2 when install on python >3.0 and <=3.4 10 | 11 | 12 | 0.2.0 (2016-06-08) 13 | ------------------ 14 | 15 | Features: 16 | 17 | * Relative time offset via ``"{% now 'utc' + 'hours=2,seconds=30' %}"`` or 18 | ``-`` to modify the rendered datetime 19 | 20 | 21 | 0.1.0 (2015-12-11) 22 | ------------------ 23 | 24 | First release on PyPI. 25 | 26 | Features: 27 | 28 | * TimeExtension with a ``now`` tag to get the current time in `jinja2`_ 29 | templates 30 | 31 | .. _`jinja2`: https://github.com/mitsuhiko/jinja2 32 | -------------------------------------------------------------------------------- /.github/workflows/run-checks.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "[0-9]+.[0-9]+.[0-9]+" 9 | - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" 10 | 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | tox: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | environment: 21 | - "py35" 22 | - "flake8" 23 | 24 | include: 25 | - environment: "py35" 26 | python: "3.5" 27 | - environment: "flake8" 28 | python: "3.5" 29 | 30 | container: 31 | image: python:${{ matrix.python }} 32 | 33 | steps: 34 | - uses: actions/checkout@master 35 | - name: Install tox 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install tox 39 | - name: Run tox 40 | run: | 41 | tox -e ${{ matrix.environment }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Raphael Pierzina 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import codecs 5 | import os 6 | 7 | from setuptools import setup 8 | 9 | 10 | def read(fname): 11 | file_path = os.path.join(os.path.dirname(__file__), fname) 12 | return codecs.open(file_path, encoding='utf-8').read() 13 | 14 | 15 | setup( 16 | name='jinja2-time', 17 | version='0.2.0', 18 | author='Raphael Pierzina', 19 | author_email='raphael@hackebrot.de', 20 | maintainer='Raphael Pierzina', 21 | maintainer_email='raphael@hackebrot.de', 22 | license='MIT', 23 | url='https://github.com/hackebrot/jinja2-time', 24 | description='Jinja2 Extension for Dates and Times', 25 | long_description=read('README.rst'), 26 | packages=[ 27 | 'jinja2_time', 28 | ], 29 | package_dir={'jinja2_time': 'jinja2_time'}, 30 | include_package_data=True, 31 | zip_safe=False, 32 | install_requires=[ 33 | 'jinja2', 34 | ], 35 | extras_require={ 36 | ":python_version=='2.7'": ["arrow"], 37 | ":python_version<='3.4'": ["arrow<=0.13.2"], 38 | ":python_version>='3.5'": ["arrow"] 39 | }, 40 | classifiers=[ 41 | 'Development Status :: 3 - Alpha', 42 | 'Intended Audience :: Developers', 43 | 'License :: OSI Approved :: MIT License', 44 | 'Natural Language :: English', 45 | 'Operating System :: OS Independent', 46 | 'Programming Language :: Python :: 2', 47 | 'Programming Language :: Python :: 2.7', 48 | 'Programming Language :: Python :: 3', 49 | 'Programming Language :: Python :: 3.3', 50 | 'Programming Language :: Python :: 3.4', 51 | 'Programming Language :: Python :: 3.5', 52 | 'Programming Language :: Python :: Implementation :: CPython', 53 | 'Programming Language :: Python :: Implementation :: PyPy', 54 | 'Programming Language :: Python', 55 | ], 56 | keywords=['jinja2', 'extension', 'time'], 57 | ) 58 | -------------------------------------------------------------------------------- /tests/test_now.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from freezegun import freeze_time 6 | from jinja2 import Environment, exceptions 7 | 8 | 9 | @pytest.fixture 10 | def environment(): 11 | return Environment(extensions=['jinja2_time.TimeExtension']) 12 | 13 | 14 | @pytest.yield_fixture(autouse=True) 15 | def freeze(): 16 | freezer = freeze_time("2015-12-09 23:33:01") 17 | freezer.start() 18 | yield 19 | freezer.stop() 20 | 21 | 22 | def test_tz_is_required(environment): 23 | with pytest.raises(exceptions.TemplateSyntaxError): 24 | environment.from_string('{% now %}') 25 | 26 | 27 | def test_utc_default_datetime_format(environment): 28 | template = environment.from_string("{% now 'utc' %}") 29 | 30 | assert template.render() == "2015-12-09" 31 | 32 | 33 | @pytest.fixture(params=['utc', 'local', 'Europe/Berlin']) 34 | def valid_tz(request): 35 | return request.param 36 | 37 | 38 | def test_accept_valid_timezones(environment, valid_tz): 39 | template = environment.from_string( 40 | "{% now '" + valid_tz + "', '%Y-%m' %}" 41 | ) 42 | 43 | assert template.render() == '2015-12' 44 | 45 | 46 | def test_environment_datetime_format(environment): 47 | environment.datetime_format = '%a, %d %b %Y %H:%M:%S' 48 | 49 | template = environment.from_string("{% now 'utc' %}") 50 | 51 | assert template.render() == "Wed, 09 Dec 2015 23:33:01" 52 | 53 | 54 | def test_add_time(environment): 55 | environment.datetime_format = '%a, %d %b %Y %H:%M:%S' 56 | 57 | template = environment.from_string( 58 | "{% now 'utc' + 'hours=2,seconds=30' %}" 59 | ) 60 | 61 | assert template.render() == "Thu, 10 Dec 2015 01:33:31" 62 | 63 | 64 | def test_substract_time(environment): 65 | environment.datetime_format = '%a, %d %b %Y %H:%M:%S' 66 | 67 | template = environment.from_string( 68 | "{% now 'utc' - 'minutes=11' %}" 69 | ) 70 | 71 | assert template.render() == "Wed, 09 Dec 2015 23:22:01" 72 | 73 | 74 | def test_offset_with_format(environment): 75 | environment.datetime_format = '%d %b %Y %H:%M:%S' 76 | 77 | template = environment.from_string( 78 | "{% now 'utc' - 'days=2, minutes=33,seconds=1', '%d %b %Y %H:%M:%S' %}" 79 | ) 80 | 81 | assert template.render() == "07 Dec 2015 23:00:00" 82 | -------------------------------------------------------------------------------- /jinja2_time/jinja2_time.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import arrow 4 | 5 | from jinja2 import nodes 6 | from jinja2.ext import Extension 7 | 8 | 9 | class TimeExtension(Extension): 10 | tags = set(['now']) 11 | 12 | def __init__(self, environment): 13 | super(TimeExtension, self).__init__(environment) 14 | 15 | # add the defaults to the environment 16 | environment.extend(datetime_format='%Y-%m-%d') 17 | 18 | def _datetime(self, timezone, operator, offset, datetime_format): 19 | d = arrow.now(timezone) 20 | 21 | # Parse replace kwargs from offset and include operator 22 | replace_params = {} 23 | for param in offset.split(','): 24 | interval, value = param.split('=') 25 | replace_params[interval.strip()] = float(operator + value.strip()) 26 | d = d.replace(**replace_params) 27 | 28 | if datetime_format is None: 29 | datetime_format = self.environment.datetime_format 30 | return d.strftime(datetime_format) 31 | 32 | def _now(self, timezone, datetime_format): 33 | if datetime_format is None: 34 | datetime_format = self.environment.datetime_format 35 | return arrow.now(timezone).strftime(datetime_format) 36 | 37 | def parse(self, parser): 38 | lineno = next(parser.stream).lineno 39 | 40 | node = parser.parse_expression() 41 | 42 | if parser.stream.skip_if('comma'): 43 | datetime_format = parser.parse_expression() 44 | else: 45 | datetime_format = nodes.Const(None) 46 | 47 | if isinstance(node, nodes.Add): 48 | call_method = self.call_method( 49 | '_datetime', 50 | [node.left, nodes.Const('+'), node.right, datetime_format], 51 | lineno=lineno, 52 | ) 53 | elif isinstance(node, nodes.Sub): 54 | call_method = self.call_method( 55 | '_datetime', 56 | [node.left, nodes.Const('-'), node.right, datetime_format], 57 | lineno=lineno, 58 | ) 59 | else: 60 | call_method = self.call_method( 61 | '_now', 62 | [node, datetime_format], 63 | lineno=lineno, 64 | ) 65 | return nodes.Output([call_method], lineno=lineno) 66 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Jinja2 Time 3 | =========== 4 | 5 | |pypi| |pyversions| |license| |travis-ci| 6 | 7 | Jinja2 Extension for Dates and Times 8 | 9 | .. |pypi| image:: https://img.shields.io/pypi/v/jinja2-time.svg 10 | :target: https://pypi.python.org/pypi/jinja2-time 11 | :alt: PyPI Package 12 | 13 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/jinja2-time.svg 14 | :target: https://pypi.python.org/pypi/jinja2-time/ 15 | :alt: PyPI Python Versions 16 | 17 | .. |license| image:: https://img.shields.io/pypi/l/jinja2-time.svg 18 | :target: https://pypi.python.org/pypi/jinja2-time 19 | :alt: PyPI Package License 20 | 21 | .. |travis-ci| image:: https://travis-ci.org/hackebrot/jinja2-time.svg?branch=master 22 | :target: https://travis-ci.org/hackebrot/jinja2-time 23 | :alt: See Build Status on Travis CI 24 | 25 | Installation 26 | ------------ 27 | 28 | **jinja2-time** is available for download from `PyPI`_ via `pip`_:: 29 | 30 | $ pip install jinja2-time 31 | 32 | It will automatically install `jinja2`_ along with `arrow`_. 33 | 34 | .. _`jinja2`: https://github.com/mitsuhiko/jinja2 35 | .. _`PyPI`: https://pypi.python.org/pypi 36 | .. _`arrow`: https://github.com/crsmithdev/arrow 37 | .. _`pip`: https://pypi.python.org/pypi/pip/ 38 | 39 | Usage 40 | ----- 41 | 42 | Now Tag 43 | ~~~~~~~ 44 | 45 | The extension comes with a ``now`` tag that provides convenient access to the 46 | `arrow.now()`_ API from your templates. 47 | 48 | You can control the output by specifying a format, that will be passed to 49 | Python's `strftime()`_: 50 | 51 | .. _`arrow.now()`: http://crsmithdev.com/arrow/#arrow.factory.ArrowFactory.now 52 | .. _`strftime()`: https://docs.python.org/3.5/library/datetime.html#strftime-and-strptime-behavior 53 | 54 | .. code-block:: python 55 | 56 | from jinja2 import Environment 57 | 58 | env = Environment(extensions=['jinja2_time.TimeExtension']) 59 | 60 | # Timezone 'local', default format -> "2015-12-10" 61 | template = env.from_string("{% now 'local' %}") 62 | 63 | # Timezone 'utc', explicit format -> "Thu, 10 Dec 2015 15:49:01" 64 | template = env.from_string("{% now 'utc', '%a, %d %b %Y %H:%M:%S' %}") 65 | 66 | # Timezone 'Europe/Berlin', explicit format -> "CET +0100" 67 | template = env.from_string("{% now 'Europe/Berlin', '%Z %z' %}") 68 | 69 | # Timezone 'utc', explicit format -> "2015" 70 | template = env.from_string("{% now 'utc', '%Y' %}") 71 | 72 | template.render() 73 | 74 | Default Datetime Format 75 | ~~~~~~~~~~~~~~~~~~~~~~~ 76 | 77 | **TimeExtension** extends the environment with a ``datetime_format`` attribute. 78 | 79 | It is used as a fallback if you omit the format for ``now``. 80 | 81 | .. code-block:: python 82 | 83 | from jinja2 import Environment 84 | 85 | env = Environment(extensions=['jinja2_time.TimeExtension']) 86 | 87 | env.datetime_format = '%a, %d %b %Y %H:%M:%S' 88 | 89 | # Timezone 'utc', default format -> "Thu, 10 Dec 2015 15:49:01" 90 | template = env.from_string("{% now 'utc' %}") 91 | 92 | template.render() 93 | 94 | Time Offset 95 | ~~~~~~~~~~~ 96 | 97 | **jinja2-time** implements a convenient interface to modify ``now`` by a 98 | relative time offset: 99 | 100 | .. code-block:: python 101 | 102 | # Examples for now "2015-12-09 23:33:01" 103 | 104 | # "Thu, 10 Dec 2015 01:33:31" 105 | "{% now 'utc' + 'hours=2, seconds=30' %}" 106 | 107 | # "Wed, 09 Dec 2015 23:22:01" 108 | "{% now 'utc' - 'minutes=11' %}" 109 | 110 | # "07 Dec 2015 23:00:00" 111 | "{% now 'utc' - 'days=2, minutes=33, seconds=1', '%d %b %Y %H:%M:%S' %}" 112 | 113 | Further documentation on the underlying functionality can be found in the 114 | `arrow replace docs`_. 115 | 116 | .. _`arrow replace docs`: http://arrow.readthedocs.io/en/latest/#replace-shift 117 | 118 | 119 | Issues 120 | ------ 121 | 122 | If you encounter any problems, please `file an issue`_ along with a detailed description. 123 | 124 | .. _`file an issue`: https://github.com/hackebrot/jinja2-time/issues 125 | 126 | 127 | Code of Conduct 128 | --------------- 129 | 130 | Everyone interacting in the jinja2-time project's codebases, issue trackers, chat 131 | rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. 132 | 133 | .. _`PyPA Code of Conduct`: https://www.pypa.io/en/latest/code-of-conduct/ 134 | 135 | License 136 | ------- 137 | 138 | Distributed under the terms of the `MIT`_ license, jinja2-time is free and open source software 139 | 140 | .. image:: https://opensource.org/trademarks/osi-certified/web/osi-certified-120x100.png 141 | :align: left 142 | :alt: OSI certified 143 | :target: https://opensource.org/ 144 | 145 | .. _`MIT`: http://opensource.org/licenses/MIT 146 | --------------------------------------------------------------------------------