Compare commits

..

1 Commits

Author SHA1 Message Date
Kenneth Reitz 4fd8baf3eb v0.1.5 2016-12-25 19:47:20 -05:00
37 changed files with 498 additions and 3312 deletions
-12
View File
@@ -1,12 +0,0 @@
[run]
branch = True
source =
maya
[paths]
source =
src
.tox/*/site-packages
[report]
show_missing = True
-46
View File
@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tuxtimo@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
-45
View File
@@ -1,45 +0,0 @@
# Contributing
Thank you for helping maya to get a better piece of software.
## Support
If you have any questions regarding the usage of maya please use the Question Issue Template or ask on [StackOverflow](https://stackoverflow.com).
## Reporting Issues / Proposing Features
Before you submit an Issue or proposing a Feature check the existing Issues in order to avoid duplicates. <br>
Please make sure you provide enough information to work on your submitted Issue or proposed Feature:
* Which version of maya are you using?
* Which version of python are you using?
* On which platform are you running maya?
Make sure to use the GitHub Template when reporting an issue.
## Pull Requests
We are very happy to receive Pull Requests considering:
* Style Guide. Follow the rules of [PEP8](http://legacy.python.org/dev/peps/pep-0008/), but you may ignore *too-long-lines* and similar warnings. There is a *pylintrc* file for more information.
* Tests. If our change affects python code inside the source code directory, please make sure your code is covered by an automated test case.
### Testing
To test the maya source code against all supported python versions you should use *tox*:
```bash
cd ~/work/maya
pip install tox
tox
```
However, if you want to test your code on certain circumstances you can create a *virtualenv*:
```
cd ~/work/maya
virtualenv env
source env/bin/activate
pip install -e '.[dev]'
commands = coverage run --parallel -m pytest -s --failed-first
```
-42
View File
@@ -1,42 +0,0 @@
---
name: Bug Report
about: Create a bug report to help us improve maya
title: ''
labels: ''
assignees: ''
---
**Important notices**
Before you add a new report, we ask you kindly to acknowledge the following:
[-] I have read the contributing guide lines at https://github.com/timofurrer/maya/blob/master/.github/CONTRIBUTING.md
[-] I have read and respect the code of conduct at https://github.com/timofurrer/maya/blob/master/.github/CODE_OF_CONDUCT.md
[-] I have searched the existing issues and I'm convinced that mine is new.
**Describe the bug**
A clear and concise description of what the bug is.
**Environment and Version**
* OS (incl. terminal and shell used): ...
* Python Version: ...
* maya Version: ...
* Your timezone: ...
**To Reproduce**
A clear and concise description of steps to reproduce the behavior
you are experiencing.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Relevant log files**
If applicable, information from log files supporting your claim.
**Additional context**
Add any other context about the problem here.
-29
View File
@@ -1,29 +0,0 @@
---
name: Feature Request
about: Suggest an idea for a new feature or enhancement for maya
title: ''
labels: ''
assignees: ''
---
**Important notices**
Before you add a new request, we ask you kindly to acknowledge the following:
[-] I have read the contributing guide lines at https://github.com/timofurrer/maya/blob/master/.github/CONTRIBUTING.md
[-] I have read and respect the code of conduct at https://github.com/timofurrer/maya/blob/master/.github/CODE_OF_CONDUCT.md
[-] I have searched the existing issues and I'm convinced that mine is new.
**Is your Feature Request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the Feature Request here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Ask a Question
about: Ask a Question about maya (Usage, Development, ...)
title: ''
labels: ''
assignees: ''
---
**Important notices**
Before you add a new report, we ask you kindly to acknowledge the following:
[-] I have read the contributing guide lines at https://github.com/timofurrer/maya/blob/master/.github/CONTRIBUTING.md
[-] I have read and respect the code of conduct at https://github.com/timofurrer/maya/blob/master/.github/CODE_OF_CONDUCT.md
[-] I have searched the existing issues and I'm convinced that mine is new.
**Ask your Question**
Ask your question here! If the question is related to a particular environment or behavior please make sure to add some context.
-82
View File
@@ -1,82 +0,0 @@
name: Continuous Integration and Deployment
on: [push]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
python-version: [2.7, 3.5, 3.6, 3.7]
os: [ubuntu-latest, macOS-latest]
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Setup build and test environment
run: |
python -m pip install --upgrade pip setuptools wheel
- name: Build Python Package
run: |
python -m pip install ".[tests]"
- name: Lint with flake8
run: |
pip install flake8
flake8 --show-source src/ tests/
- name: Check Manifest
run: |
pip install check-manifest
check-manifest
- name: Test with pytest
run: |
coverage run --parallel -m pytest
- name: Report code coverage
run: |
coverage combine
coverage report
coverage xml
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Setup docs environment
run: |
python -m pip install ".[docs]"
- name: Build documentation with sphinx
run: |
sphinx-build -W -b html -d doctrees docs/source docs/_build/html
publish:
needs: [build, docs]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
if: startsWith(github.event.ref, 'refs/tags')
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Build Package
if: startsWith(github.event.ref, 'refs/tags')
run: |
python -m pip install --upgrade pip setuptools wheel
python setup.py sdist bdist_wheel --universal
- name: Publish Package on PyPI
if: startsWith(github.event.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_token }}
-18
View File
@@ -1,18 +0,0 @@
name: "Close Stale Issues and Pull Requests"
on:
schedule:
- cron: "0 * * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
stale-issue-label: stale
stale-pr-label: state
stale-issue-message: 'This Issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
stale-pr-message: 'This Pull Request is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
-1
View File
@@ -24,7 +24,6 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
Pipfile.lock
# PyInstaller
# Usually these files are written by a python script from a template
-32
View File
@@ -1,32 +0,0 @@
repos:
- repo: https://github.com/ambv/black
rev: 19.3b0
hooks:
- id: black
language_version: python3.7
# override until resolved: https://github.com/ambv/black/issues/402
files: \.pyi?$
types: []
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.7
hooks:
- id: flake8
language_version: python3.7
#- repo: https://github.com/asottile/seed-isort-config
#rev: v1.9.1
#hooks:
#- id: seed-isort-config
#- repo: https://github.com/pre-commit/mirrors-isort
#rev: v4.3.20
#hooks:
#- id: isort
#language_version: python3.7
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.2.3
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
+8
View File
@@ -0,0 +1,8 @@
language: python
python:
- "2.7"
# - "3.5" # looks like ruamel.ordereddict doesn't build for python3
# command to install dependencies
install: "pip install -r requirements.txt"
# command to run tests
script: make
+19
View File
@@ -0,0 +1,19 @@
# Contributions to the maya project
## Creator & Maintainer
- Kenneth Reitz <me@kennethreitz.org> `@kennethreitz <https://github.com/kennethreitz>`_
## Contributors
In chronological order:
- Adam Nelson <adam@varud.com> (`@adamn <https://github.com/adamn>`_)
- Timo Furrer <tuxtimo@gmail.com> (`@timofurrer <https://github.com/timofurrer>`_)
- Moinuddin Quadri <moin18@gmail.com> (`@moin18 <https://github.com/moin18>`_)
- Grigouze <grigouze@yahoo.fr> (`@grigouze <https://github.com/grigouze>`_)
- Tzu-ping Chung <uranusjr@gmail.com> (`@uranusjr <https://github.com/uranusjr>`_)
- aaronjeline (`@aaronjeline <https://github.com/aaronjeline>`_)
- jerry2yu (`@jerry2yu <https://github.com/jerry2yu>`_)
-507
View File
@@ -1,507 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Maya: Datetimes for Humans™"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Datetimes are very frustrating to work with in Python, especially when dealing with different locales on different systems. This library exists to make the simple things much easier, while admitting that time is an illusion (timezones doubly so).\n",
"\n",
"Datetimes should be interacted with via an API written for humans.\n",
"\n",
"Maya is mostly built around the headaches and use-cases around parsing datetime data from websites."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic Usage of Maya"
]
},
{
"cell_type": "code",
"execution_count": 33,
"metadata": {},
"outputs": [],
"source": [
"import maya"
]
},
{
"cell_type": "code",
"execution_count": 34,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<MayaDT epoch=1544809514.491009>"
]
},
"execution_count": 34,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"maya.now()"
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<MayaDT epoch=1544895914.872356>"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tomorrow = maya.when('tomorrow')\n",
"tomorrow"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'tomorrow'"
]
},
"execution_count": 36,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tomorrow.slang_date()"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'in 23 hours'"
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tomorrow.slang_time()"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'2018-12-15T17:45:14.872356Z'"
]
},
"execution_count": 38,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Also: MayaDT.from_iso8601(...)\n",
"tomorrow.iso8601()"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Sat, 15 Dec 2018 17:45:14 GMT'"
]
},
"execution_count": 39,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Also: MayaDT.from_rfc2822(...)\n",
"tomorrow.rfc2822()"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'2018-12-15T17:45:14.8Z'"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Also: MayaDT.from_rfc3339(...)\n",
"tomorrow.rfc3339()"
]
},
{
"cell_type": "code",
"execution_count": 41,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"datetime.datetime(2018, 12, 15, 17, 45, 14, 872356, tzinfo=<UTC>)"
]
},
"execution_count": 41,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tomorrow.datetime()"
]
},
{
"cell_type": "code",
"execution_count": 42,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"datetime.datetime(2016, 12, 16, 13, 23, 45, 423992)"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Automatically parse datetime strings and generate naive datetimes.\n",
"scraped = '2016-12-16 18:23:45.423992+00:00'\n",
"maya.parse(scraped).datetime(to_timezone='US/Eastern', naive=True)"
]
},
{
"cell_type": "code",
"execution_count": 43,
"metadata": {},
"outputs": [],
"source": [
"rand_day = maya.when('2011-02-07', timezone='US/Eastern')"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<MayaDT epoch=1544809518.275386>"
]
},
"execution_count": 44,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Maya speaks Python.\n",
"from datetime import datetime\n",
"\n",
"maya.MayaDT.from_datetime(datetime.utcnow())"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<MayaDT epoch=1544809518.0>"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import time\n",
"\n",
"maya.MayaDT.from_struct(time.gmtime())"
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<MayaDT epoch=1544809520.426774>"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"maya.MayaDT(time.time())"
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"7"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"rand_day.day"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"17"
]
},
"execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"rand_day.add(days=10).day"
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'UTC'"
]
},
"execution_count": 49,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Always.\n",
"rand_day.timezone"
]
},
{
"cell_type": "code",
"execution_count": 50,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<generator object intervals at 0x10c54a308>"
]
},
"execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Range of hours in a day:\n",
"maya.intervals(start=maya.now(), end=maya.now().add(days=1), interval=60*60)"
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Mon, 21 Feb 1994 03:00:00 GMT'"
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# snap modifiers\n",
"dt = maya.when('Mon, 21 Feb 1994 21:21:42 GMT')\n",
"dt.snap('@d+3h').rfc2822()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Advanced Usage of Maya"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In addition to timestamps, Maya also includes a wonderfully powerful MayaInterval class, which represents a range of time (e.g. an event). With this class, you can perform a multitude of advanced calendar calculations with finesse and ease.\n",
"\n",
"For example:"
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {},
"outputs": [],
"source": [
"from maya import MayaInterval"
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {},
"outputs": [],
"source": [
"# Create an event that is one hour long, starting now.\n",
"event_start = maya.now()\n",
"event_end = event_start.add(hours=1)"
]
},
{
"cell_type": "code",
"execution_count": 54,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"<MayaInterval start=<MayaDT epoch=1544809524.395196> end=<MayaDT epoch=1544813124.395196>>"
]
},
"execution_count": 54,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"MayaInterval(start=event_start, end=event_end)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"From here, there are a number of methods available to you, which you can use to compare this event to another event."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Do your own experiments here...\n",
"\n",
"Try `maya` youself by adding your code below and running your own experiments 👇"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import maya\n",
"\n",
"# your code here\n",
"maya."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+3 -17
View File
@@ -1,21 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016 Kenneth Reitz
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:
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 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.
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.
-30
View File
@@ -1,30 +0,0 @@
# Additional package data
# Metadata
include LICENSE *.md *.rst *.toml *.yml *.yaml
graft .github
# Jupyter Notebooks
include *.ipynb
# Stubs
recursive-include src *.pyi
# Tests
include tox.ini .coveragerc conftest.py
recursive-include tests *.py
# Documentation
include docs/Makefile docs/docutils.conf
recursive-include docs *.bat
recursive-include docs *.png
recursive-include docs *.svg
recursive-include docs *.py
recursive-include docs *.rst
recursive-include docs *.ico
prune docs/_build
# Just to keep check-manifest happy; on releases those files are gone.
# Last rule wins!
exclude changelog.d/*.rst
include changelog.d/towncrier_template.rst
+2
View File
@@ -0,0 +1,2 @@
tests:
pytest test_maya.py
+21 -77
View File
@@ -1,11 +1,14 @@
Maya: Datetimes for Humans™
===========================
Maya: Datetime for Humans™
==========================
.. image:: https://img.shields.io/pypi/v/maya.svg
:target: https://pypi.python.org/pypi/maya
.. image:: https://github.com/timofurrer/maya/workflows/Continuous%20Integration%20and%20Deployment/badge.svg
:target: https://github.com/timofurrer/maya/actions
.. image:: https://travis-ci.org/kennethreitz/maya.svg?branch=master
:target: https://travis-ci.org/kennethreitz/maya
.. image:: https://img.shields.io/badge/SayThanks.io-☼-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
Datetimes are very frustrating to work with in Python, especially when dealing
@@ -37,17 +40,11 @@ Behold, datetimes for humans!
>>> tomorrow.slang_time()
'23 hours from now'
# Also: MayaDT.from_iso8601(...)
>>> tomorrow.iso8601()
'2017-02-10T22:17:01.445418Z'
'2016-12-16T15:11:30.263350Z'
# Also: MayaDT.from_rfc2822(...)
>>> tomorrow.rfc2822()
'Fri, 10 Feb 2017 22:17:01 GMT'
# Also: MayaDT.from_rfc3339(...)
>>> tomorrow.rfc3339()
'2017-02-10T22:17:01.44Z'
'Fri, 16 Dec 2016 20:11:30 -0000'
>>> tomorrow.datetime()
datetime.datetime(2016, 12, 16, 15, 11, 30, 263350, tzinfo=<UTC>)
@@ -60,59 +57,14 @@ Behold, datetimes for humans!
>>> rand_day = maya.when('2011-02-07', timezone='US/Eastern')
<MayaDT epoch=1297036800.0>
# Maya speaks Python.
>>> m = maya.MayaDT.from_datetime(datetime.utcnow())
>>> print(m)
Wed, 20 Sep 2017 17:24:32 GMT
>>> m = maya.MayaDT.from_struct(time.gmtime())
>>> print(m)
Wed, 20 Sep 2017 17:24:32 GMT
>>> m = maya.MayaDT(time.time())
>>> print(m)
Wed, 20 Sep 2017 17:24:32 GMT
# Note how this is the 6th, not the 7th.
>>> rand_day.day
7
>>> rand_day.add(days=10).day
17
6
# Always.
>>> rand_day.timezone
UTC
# Range of hours in a day:
>>> maya.intervals(start=maya.now(), end=maya.now().add(days=1), interval=60*60)
<generator object intervals at 0x105ba5820>
# snap modifiers
>>> dt = maya.when('Mon, 21 Feb 1994 21:21:42 GMT')
>>> dt.snap('@d+3h').rfc2822()
'Mon, 21 Feb 1994 03:00:00 GMT'
☤ Advanced Usage of Maya
------------------------
In addition to timestamps, Maya also includes a wonderfully powerful ``MayaInterval`` class, which represents a range of time (e.g. an event). With this class, you can perform a multitude of advanced calendar calculations with finesse and ease.
For example:
.. code-block:: pycon
>>> from maya import MayaInterval
# Create an event that is one hour long, starting now.
>>> event_start = maya.now()
>>> event_end = event_start.add(hours=1)
>>> event = MayaInterval(start=event_start, end=event_end)
From here, there are a number of methods available to you, which you can use to compare this event to another event.
☤ Why is this useful?
---------------------
@@ -125,38 +77,29 @@ From here, there are a number of methods available to you, which you can use to
- Maya never panics, and always carries a towel.
☤ What about Delorean_, Arrow_, & Pendulum_?
☤ What about Delorean, Arrow, & Pendulum?
-----------------------------------------
All these projects complement each other, and are friends. Pendulum, for example, helps power Maya's parsing.
Arrow, for example, is a fantastic library, but isn't what I wanted in a datetime library. In many ways, it's better than Maya for certain things. In some ways, in my opinion, it's not.
I simply desire a sane API for datetimes that made sense to me for all the things I'd ever want to do—especially when dealing with timezone algebra. Arrow doesn't do all of the things I need (but it does a lot more!). Maya does do exactly what I need.
I think these projects complement each-other, personally. Maya is great for parsing websites, and dealing with calendar events!
.. _Delorean: https://delorean.readthedocs.io/
.. _Arrow: https://arrow.readthedocs.io/
.. _Pendulum: https://pendulum.eustace.io/
I think these projects complement each-other, personally. Maya is great for parsing websites. For example- Arrow supports floors and ceilings and spans of dates, which Maya does not at all.
☤ Installing Maya
-----------------
Installation is easy, with:
Installation is easy, with pip::
$ pip install maya
✨🍰✨
Demo
------
Like it?
----------
Try ``maya`` interactively using this online demo:
.. image:: https://user-images.githubusercontent.com/1155573/49400125-16e73500-f722-11e8-9275-e1d7eb3bbf99.png
:target: https://notebooks.ai/demo/gh/kennethreitz/maya
:alt: Open Live Demo
`Say Thanks <https://saythanks.io/to/kennethreitz>`_!
How to Contribute
@@ -165,6 +108,7 @@ How to Contribute
#. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug.
#. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it).
#. Write a test which shows that the bug was fixed or that the feature works as expected.
#. Send a pull request and bug the maintainer until it gets merged and published. :)
#. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS_.
.. _`the repository`: http://github.com/timofurrer/maya
.. _`the repository`: http://github.com/kennethreitz/maya
.. _AUTHORS: https://github.com/kennethreitz/maya/blob/master/AUTHORS.rst
-2
View File
@@ -1,2 +0,0 @@
requirements:
- maya==0.6.0
-20
View File
@@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = maya
SOURCEDIR = source
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-36
View File
@@ -1,36 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=python -msphinx
)
set SOURCEDIR=source
set BUILDDIR=build
set SPHINXPROJ=maya
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The Sphinx module was not found. Make sure you have Sphinx installed,
echo.then set the SPHINXBUILD environment variable to point to the full
echo.path of the 'sphinx-build' executable. Alternatively you may add the
echo.Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd
-162
View File
@@ -1,162 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# maya documentation build configuration file, created by
# sphinx-quickstart on Sun May 28 15:46:10 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
# sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath(".."))
sys.path.insert(0, os.path.abspath("_themes"))
import maya # noqa
# -- 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.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "maya"
copyright = "2017, Kenneth Reitz"
author = "Kenneth Reitz"
# 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 = maya.__version__
# The full version, including alpha/beta/rc tags.
release = maya.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ["_static"]
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "mayadoc"
# -- 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, "maya.tex", "maya Documentation", "Kenneth Reitz", "manual")
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "maya", "maya Documentation", [author], 1)]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"maya",
"maya Documentation",
author,
"maya",
"One line description of project.",
"Miscellaneous",
)
]
-82
View File
@@ -1,82 +0,0 @@
.. maya documentation master file, created by
sphinx-quickstart on Sun May 28 15:46:10 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Maya: Datetime for Humans
================================
Release v\ |version|. (:ref:`Installation <install>`)
.. image:: https://img.shields.io/pypi/v/maya.svg
:target: https://pypi.python.org/pypi/maya
.. image:: https://travis-ci.org/kennethreitz/maya.svg?branch=master
:target: https://travis-ci.org/kennethreitz/maya
.. image:: https://img.shields.io/badge/SayThanks-!-1EAEDB.svg
:target: https://saythanks.io/to/kennethreitz
☤ Behold, datetimes for humans!
-------------------------------
.. code-block:: pycon
>>> now = maya.now()
<MayaDT epoch=1481850660.9>
>>> tomorrow = maya.when('tomorrow')
<MayaDT epoch=1481919067.23>
>>> tomorrow.slang_date()
'tomorrow'
>>> tomorrow.slang_time()
'23 hours from now'
# Also: MayaDT.from_iso8601(...)
>>> tomorrow.iso8601()
'2017-02-10T22:17:01.445418Z'
# Also: MayaDT.from_rfc2822(...)
>>> tomorrow.rfc2822()
'Fri, 10 Feb 2017 22:17:01 GMT'
# Also: MayaDT.from_rfc3339(...)
>>> tomorrow.rfc3339()
'2017-02-10T22:17:01.44Z'
>>> tomorrow.datetime()
datetime.datetime(2016, 12, 16, 15, 11, 30, 263350, tzinfo=<UTC>)
# Automatically parse datetime strings and generate naive datetimes.
>>> scraped = '2016-12-16 18:23:45.423992+00:00'
>>> maya.parse(scraped).datetime(to_timezone='US/Eastern', naive=True)
datetime.datetime(2016, 12, 16, 13, 23, 45, 423992)
>>> rand_day = maya.when('2011-02-07', timezone='US/Eastern')
<MayaDT epoch=1297036800.0>
>>> rand_day.day
7
>>> rand_day.add(days=10).day
17
# Always.
>>> rand_day.timezone
UTC
# Range of hours in a day:
>>> maya.interval(
... start=maya.now(),
... end=maya.now().add(days=1),
... interval=60*60)
<generator object intervals at 0x105ba5820>
Table of Contents
-----------------
.. toctree::
:maxdepth: 2
user/install
user/quickstart
-29
View File
@@ -1,29 +0,0 @@
.. _install:
Installation
============
Pip Install Maya
----------------
To install maya, simply use::
$ pip install maya
Source Code
-----------
Maya is actively developed on `Github
<https://github.com/kennethreitz/maya.git>`_
You can either clone the public repository::
$ git clone git://github.com/kennethreitz/maya.git
Or, download the `tarball <https://github.com/kennethreitz/maya/tarball/master>`_::
$ curl -OL https://github.com/kennethreitz/maya/tarball/master
# optionally, zipball is also available (for Windows users).
Once you have a copy of the source, you can embed it in your own Python
package, or install it into your site-packages easily::
$ python setup.py install
-30
View File
@@ -1,30 +0,0 @@
.. _quickstart:
Quickstart
==========
.. module::maya
Ready for a simple datetime tool? This doc provides some tools to use in your
busy workflow.
Parse a Date
------------
Parsing a date from a string with Maya is 🍰!
First, you'll need to import maya::
>>> import maya
There are currently two ways to make sense of datetime:
- ``maya.parse``
- ``maya.when``
A simple answer is that you should use parse on machine output, and when on human input.
Use as follows::
>>> recent_win = maya.parse('2016-11-02T20:00PM')
>>> old_win = maya.when('October 14, 1908')
>>> grandpas_date = maya.when('108 years ago')
Executable
+265
View File
@@ -0,0 +1,265 @@
# ___ __ ___ _ _ ___
# || \/ | ||=|| \\// ||=||
# || | || || // || ||
# Ignore warnings for yaml usage.
import warnings
import ruamel.yaml
warnings.simplefilter('ignore', ruamel.yaml.error.UnsafeLoaderWarning)
import email.utils
import time
from datetime import datetime as Datetime
import pytz
import humanize
import dateparser
import iso8601
import dateutil.parser
from tzlocal import get_localzone
_EPOCH_START = (1970, 1, 1)
def validate_type_mayadt(func):
"""
Decorator to validate all the arguments to function
are of type `MayaDT`
"""
def inner(*args, **kwargs):
for arg in args + tuple(kwargs.values()):
if not isinstance(arg, MayaDT):
raise ValueError("Operation allowed only on object of type '{}'".format(MayaDT.__name__))
return func(*args, **kwargs)
return inner
class MayaDT(object):
"""The Maya Datetime object."""
def __init__(self, epoch):
super(MayaDT, self).__init__()
self._epoch = epoch
def __repr__(self):
return '<MayaDT epoch={}>'.format(self._epoch)
def __format__(self, *args, **kwargs):
"""Return's the datetime's format"""
return format(self.datetime(), *args, **kwargs)
@validate_type_mayadt
def __sub__(self, maya_dt):
return MayaDT(self._epoch - maya_dt._epoch)
@validate_type_mayadt
def __eq__(self, maya_dt):
return self._epoch == maya_dt._epoch
@validate_type_mayadt
def __ne__(self, maya_dt):
return not self.__eq__(maya_dt)
@validate_type_mayadt
def __lt__(self, maya_dt):
return self._epoch < maya_dt._epoch
@validate_type_mayadt
def __le__(self, maya_dt):
return self.__lt__(maya_dt) or self.__eq__(maya_dt)
@validate_type_mayadt
def __gt__(self, maya_dt):
return self._epoch > maya_dt._epoch
@validate_type_mayadt
def __ge__(self, maya_dt):
return self.__gt__(maya_dt) or self.__eq__(maya_dt)
# Timezone Crap
# -------------
@property
def timezone(self):
"""Returns the UTC tzinfo name. It's always UTC. Always."""
return 'UTC'
@property
def _tz(self):
"""Returns the UTC tzinfo object."""
return pytz.timezone(self.timezone)
@property
def local_timezone(self):
"""Returns the name of the local timezone, for informational purposes."""
return self._local_tz.zone
@property
def _local_tz(self):
"""Returns the local timezone."""
return get_localzone()
@staticmethod
def __dt_to_epoch(dt):
"""Converts a datetime into an epoch."""
# Assume UTC if no datetime is provided.
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.utc)
epoch_start = Datetime(*_EPOCH_START, tzinfo=pytz.timezone('UTC'))
return (dt - epoch_start).total_seconds()
# Importers
# ---------
@classmethod
def from_datetime(klass, dt):
"""Returns MayaDT instance from datetime."""
return klass(klass.__dt_to_epoch(dt))
@classmethod
def from_iso8601(klass, string):
"""Returns MayaDT instance from iso8601 string."""
dt = iso8601.parse_date(string)
return klass.from_datetime(dt)
@staticmethod
def from_rfc2822(string):
"""Returns MayaDT instance from rfc2822 string."""
return parse(string)
# Exporters
# ---------
def datetime(self, to_timezone=None, naive=False):
"""Returns a timezone-aware datetime...
Defaulting to UTC (as it should).
Keyword Arguments:
to_timezone {string} -- timezone to convert to (default: None/UTC)
naive {boolean} -- if True, the tzinfo is simply dropped (default: False)
"""
if to_timezone:
dt = self.datetime().astimezone(pytz.timezone(to_timezone))
else:
dt = Datetime.utcfromtimestamp(self._epoch)
dt.replace(tzinfo=self._tz)
# Strip the timezone info if requested to do so.
if naive:
return dt.replace(tzinfo=None)
else:
if dt.tzinfo is None:
dt = dt.replace(tzinfo=self._tz)
return dt
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaDT."""
# Get a timezone-naive datetime.
dt = self.datetime(naive=True)
return '{}Z'.format(dt.isoformat())
def rfc2822(self):
"""Returns an RFC 2822 representation of the MayaDT."""
return email.utils.formatdate(self.epoch, usegmt=True)
# Properties
# ----------
@property
def year(self):
return self.datetime().year
@property
def month(self):
return self.datetime().month
@property
def day(self):
return self.datetime().day
@property
def week(self):
return self.datetime().isocalendar()[1]
@property
def weekday(self):
"""Return the day of the week as an integer. Monday is 1 and Sunday is 7"""
return self.datetime().isoweekday()
@property
def hour(self):
return self.datetime().hour
@property
def minute(self):
return self.datetime().minute
@property
def second(self):
return self.datetime().second
@property
def microsecond(self):
return self.datetime().microsecond
@property
def epoch(self):
return self._epoch
# Human Slang Extras
# ------------------
def slang_date(self):
""""Returns human slang representation of date."""
return humanize.naturaldate(self.datetime())
def slang_time(self):
""""Returns human slang representation of time."""
dt = self.datetime(naive=True, to_timezone=self.local_timezone)
return humanize.naturaltime(dt)
def now():
"""Returns a MayaDT instance for this exact moment."""
epoch = time.time()
return MayaDT(epoch=epoch)
def when(string, timezone='UTC'):
""""Returns a MayaDT instance for the human moment specified.
Powered by dateparser. Useful for scraping websites.
Examples:
'next week', 'now', 'tomorrow', '300 years ago', 'August 14, 2015'
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: 'UTC')
"""
dt = dateparser.parse(string, settings={'TIMEZONE': timezone, 'RETURN_AS_TIMEZONE_AWARE': True, 'TO_TIMEZONE': 'UTC'})
if dt is None:
raise ValueError('invalid datetime input specified.')
return MayaDT.from_datetime(dt)
def parse(string, day_first=False):
""""Returns a MayaDT instance for the machine-produced moment specified.
Powered by dateutil. Accepts most known formats. Useful for working with data.
Keyword Arguments:
string -- string to be parsed
day_first -- if true, the first value (e.g. 01/05/2016) is parsed as day (default: False)
"""
dt = dateutil.parser.parse(string, dayfirst=day_first)
return MayaDT.from_datetime(dt)
+16
View File
@@ -0,0 +1,16 @@
-e .
dateparser==0.5.0
humanize==0.5.1
iso8601==0.1.11
jdatetime==1.8.1
py==1.4.32
pytest==3.0.5
python-dateutil==2.6.0
pytz==2016.10
regex==2016.11.21
ruamel.ordereddict==0.4.9
ruamel.yaml==0.13.4
six==1.10.0
typing==3.5.2.2
tzlocal==1.3
umalqurra==0.2
-18
View File
@@ -1,18 +0,0 @@
[bdist_wheel]
universal = 1
[metadata]
# ensure LICENSE is included in wheel metadata
license_file = LICENSE
[flake8]
max-line-length = 100
ignore = E203
[tool:pytest]
testpaths = tests/
[isort]
known_first_party=maya
known_third_party=humanize,pytz,dateparser,tzlocal,pendulum,snaptime
multi_line_output=3
Regular → Executable
+43 -85
View File
@@ -1,92 +1,50 @@
import codecs
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
import sys
import codecs
from setuptools import find_packages, setup
from setuptools import setup
#: Holds a list of packages to install with the binary distribution
PACKAGES = find_packages(where="src")
META_FILE = os.path.abspath("src/maya/__init__.py")
KEYWORDS = ["datetime", "timezone", "scrape", "web"]
CLASSIFIERS = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development :: Libraries :: Python Modules",
try:
# Python 3
from os import dirname
except ImportError:
# Python 2
from os.path import dirname
here = os.path.abspath(dirname(__file__))
def read(*parts):
return codecs.open(os.path.join(here, *parts), 'r').read()
if sys.argv[-1] == "publish":
os.system("python setup.py sdist bdist_wheel upload")
sys.exit()
required = [
'humanize',
'pytz',
'dateparser',
'iso8601',
'python-dateutil',
'ruamel.yaml',
'tzlocal'
]
#: Holds the runtime requirements for the end user
INSTALL_REQUIRES = [
"humanize",
"pytz",
"dateparser>=0.7.0",
"tzlocal",
"pendulum>=2.0.2",
"snaptime",
]
#: Holds runtime requirements and development requirements
EXTRAS_REQUIRES = {
# extras for contributors
"docs": ["sphinx"],
"tests": ["freezegun", "coverage", "pytest", "pytest-mock"],
}
EXTRAS_REQUIRES["dev"] = (
EXTRAS_REQUIRES["tests"] + EXTRAS_REQUIRES["docs"] + ["pre-commit"]
)
#: Holds the contents of the README file
with codecs.open("README.rst", encoding="utf-8") as readme:
__README_CONTENTS__ = readme.read()
def read(metafile):
"""
Return the contents of the given meta data file assuming UTF-8 encoding.
"""
with codecs.open(str(metafile), encoding="utf-8") as f:
return f.read()
def get_meta(meta, metafile):
"""
Extract __*meta*__ from the given metafile.
"""
contents = read(metafile)
meta_match = re.search(
r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), contents, re.M
)
if meta_match:
return meta_match.group(1)
raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta))
setup(
name="maya",
version=get_meta("version", META_FILE),
license=get_meta("license", META_FILE),
description=get_meta("description", META_FILE),
long_description=__README_CONTENTS__,
author=get_meta("author", META_FILE),
author_email=get_meta("author_email", META_FILE),
maintainer=get_meta("author", META_FILE),
maintainer_email=get_meta("author_email", META_FILE),
platforms=["Linux", "Windows", "MAC OS X"],
url=get_meta("url", META_FILE),
download_url=get_meta("download_url", META_FILE),
bugtrack_url=get_meta("bugtrack_url", META_FILE),
packages=PACKAGES,
package_dir={"": "src"},
include_package_data=True,
install_requires=INSTALL_REQUIRES,
extras_require=EXTRAS_REQUIRES,
keywords=KEYWORDS,
classifiers=CLASSIFIERS,
name='maya',
version='0.1.5',
description='Datetimes for Humans.',
long_description= '\n' + read('README.rst'),
author='Kenneth Reitz',
author_email='me@kennethreitz.com',
url='https://github.com/kennethreitz/maya',
py_modules=['maya'],
install_requires=required,
license='MIT',
classifiers=(
),
)
-10
View File
@@ -1,10 +0,0 @@
__description__ = "Datetimes for Humans™"
__license__ = "MIT"
__version__ = "0.6.0a1"
__author__ = "Timo Furrer"
__author_email__ = "tuxtimo@gmail.com"
__url__ = "http://github.com/timofurrer/maya"
__download_url__ = "https://github.com/timofurrer/maya"
__bugtrack_url__ = "https://github.com/timofurrer/maya/issues"
from .core import * # noqa
-103
View File
@@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
"""
maya.compat
~~~~~~~~~~~~~~~
This module handles import compatibility issues between Python 2 and
Python 3.
"""
import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
# : Python 2.x?
is_py2 = _ver[0] == 2
# : Python 3.x?
is_py3 = _ver[0] == 3
# ---------
# Specifics
# ---------
if is_py2:
cmp = cmp # noqa
elif is_py3:
def cmp(a, b):
"""
Compare two objects.
Returns a negative number if C{a < b}, zero if they are equal, and a
positive number if C{a > b}.
"""
if a < b:
return -1
elif a == b:
return 0
else:
return 1
def comparable(klass):
"""
Class decorator that ensures support for the special C{__cmp__} method.
On Python 2 this does nothing.
On Python 3, C{__eq__}, C{__lt__}, etc. methods are added to the class,
relying on C{__cmp__} to implement their comparisons.
"""
# On Python 2, __cmp__ will just work, so no need to add extra methods:
if not is_py3:
return klass
def __eq__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c == 0
def __ne__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c != 0
def __lt__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c < 0
def __le__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c <= 0
def __gt__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c > 0
def __ge__(self, other):
c = self.__cmp__(other)
if c is NotImplemented:
return c
return c >= 0
klass.__lt__ = __lt__
klass.__gt__ = __gt__
klass.__le__ = __le__
klass.__ge__ = __ge__
klass.__eq__ = __eq__
klass.__ne__ = __ne__
return klass
-778
View File
@@ -1,778 +0,0 @@
# ___ __ ___ _ _ ___
# || \/ | ||=|| \\// ||=||
# || | || || // || ||
import email.utils
import time
import functools
from datetime import timedelta, datetime as Datetime
import re
import pytz
import humanize
import dateparser
import pendulum
import snaptime
from tzlocal import get_localzone
from dateutil.relativedelta import relativedelta
from dateparser.languages.loader import default_loader
from .compat import cmp, comparable
def validate_class_type_arguments(operator):
"""
Decorator to validate all the arguments to function
are of the type of calling class for passed operator
"""
def inner(function):
def wrapper(self, *args, **kwargs):
for arg in args + tuple(kwargs.values()):
if not isinstance(arg, self.__class__):
raise TypeError(
"unorderable types: {}() {} {}()".format(
type(self).__name__, operator, type(arg).__name__
)
)
return function(self, *args, **kwargs)
return wrapper
return inner
def validate_arguments_type_of_function(param_type=None):
"""
Decorator to validate the <type> of arguments in
the calling function are of the `param_type` class.
if `param_type` is None, uses `param_type` as the class where it is used.
Note: Use this decorator on the functions of the class.
"""
def inner(function):
def wrapper(self, *args, **kwargs):
type_ = param_type or type(self)
for arg in args + tuple(kwargs.values()):
if not isinstance(arg, type_):
raise TypeError(
(
"Invalid Type: {}.{}() accepts only the "
'arguments of type "<{}>"'
).format(type(self).__name__, function.__name__, type_.__name__)
)
return function(self, *args, **kwargs)
return wrapper
return inner
class MayaDT(object):
"""The Maya Datetime object."""
__EPOCH_START = (1970, 1, 1)
def __init__(self, epoch):
super(MayaDT, self).__init__()
# round the given epoch's microseconds to 6 digits
# to be compatible with Python's datetime.microseconds.
# See: https://docs.python.org/3/library/datetime.html#datetime.datetime.microsecond
self._epoch = round(epoch, 6)
def __repr__(self):
return "<MayaDT epoch={}>".format(self._epoch)
def __str__(self):
return self.rfc2822()
def __format__(self, *args, **kwargs):
"""Return's the datetime's format"""
return format(self.datetime(), *args, **kwargs)
@validate_class_type_arguments("==")
def __eq__(self, maya_dt):
return self._epoch == maya_dt._epoch
@validate_class_type_arguments("!=")
def __ne__(self, maya_dt):
return self._epoch != maya_dt._epoch
@validate_class_type_arguments("<")
def __lt__(self, maya_dt):
return self._epoch < maya_dt._epoch
@validate_class_type_arguments("<=")
def __le__(self, maya_dt):
return self._epoch <= maya_dt._epoch
@validate_class_type_arguments(">")
def __gt__(self, maya_dt):
return self._epoch > maya_dt._epoch
@validate_class_type_arguments(">=")
def __ge__(self, maya_dt):
return self._epoch >= maya_dt._epoch
def __hash__(self):
return hash(self.epoch)
def __add__(self, duration):
return self.add(seconds=_seconds_or_timedelta(duration).total_seconds())
def __radd__(self, duration):
return self + duration
def __sub__(self, duration_or_date):
if isinstance(duration_or_date, MayaDT):
return self.subtract_date(dt=duration_or_date)
else:
return self.subtract(
seconds=_seconds_or_timedelta(duration_or_date).total_seconds()
)
def add(self, **kwargs):
"""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).add(**kwargs))
def subtract(self, **kwargs):
"""Returns a new MayaDT object with the given offsets."""
return self.from_datetime(pendulum.instance(self.datetime()).subtract(**kwargs))
def subtract_date(self, **kwargs):
"""Returns a timedelta object with the duration between the dates"""
return timedelta(seconds=self.epoch - kwargs["dt"].epoch)
def snap(self, instruction):
"""
Returns a new MayaDT object modified by the given instruction.
Powered by snaptime. See https://github.com/zartstrom/snaptime
for a complete documentation about the snaptime instructions.
"""
return self.from_datetime(snaptime.snap(self.datetime(), instruction))
# Timezone Crap
# -------------
@property
def timezone(self):
"""Returns the UTC tzinfo name. It's always UTC. Always."""
return "UTC"
@property
def _tz(self):
"""Returns the UTC tzinfo object."""
return pytz.timezone(self.timezone)
@property
def local_timezone(self):
"""Returns the name of the local timezone."""
if self._local_tz.zone in pytz.all_timezones:
return self._local_tz.zone
return self.timezone
@property
def _local_tz(self):
"""Returns the local timezone."""
return get_localzone()
@staticmethod
@validate_arguments_type_of_function(Datetime)
def __dt_to_epoch(dt):
"""Converts a datetime into an epoch."""
# Assume UTC if no datetime is provided.
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.utc)
epoch_start = Datetime(*MayaDT.__EPOCH_START, tzinfo=pytz.timezone("UTC"))
return (dt - epoch_start).total_seconds()
# Importers
# ---------
@classmethod
@validate_arguments_type_of_function(Datetime)
def from_datetime(klass, dt):
"""Returns MayaDT instance from datetime."""
return klass(klass.__dt_to_epoch(dt))
@classmethod
@validate_arguments_type_of_function(time.struct_time)
def from_struct(klass, struct, timezone=pytz.UTC):
"""Returns MayaDT instance from a 9-tuple struct
It's assumed to be from gmtime().
"""
struct_time = time.mktime(struct) - utc_offset(struct)
dt = Datetime.fromtimestamp(struct_time, timezone)
return klass(klass.__dt_to_epoch(dt))
@classmethod
def from_iso8601(klass, iso8601_string):
"""Returns MayaDT instance from iso8601 string."""
return parse(iso8601_string)
@staticmethod
def from_rfc2822(rfc2822_string):
"""Returns MayaDT instance from rfc2822 string."""
return parse(rfc2822_string)
@staticmethod
def from_rfc3339(rfc3339_string):
"""Returns MayaDT instance from rfc3339 string."""
return parse(rfc3339_string)
# Exporters
# ---------
def datetime(self, to_timezone=None, naive=False):
"""Returns a timezone-aware datetime...
Defaulting to UTC (as it should).
Keyword Arguments:
to_timezone {str} -- timezone to convert to (default: None/UTC)
naive {bool} -- if True,
the tzinfo is simply dropped (default: False)
"""
if to_timezone:
dt = self.datetime().astimezone(pytz.timezone(to_timezone))
else:
dt = Datetime.utcfromtimestamp(self._epoch)
dt.replace(tzinfo=self._tz)
# Strip the timezone info if requested to do so.
if naive:
return dt.replace(tzinfo=None)
else:
if dt.tzinfo is None:
dt = dt.replace(tzinfo=self._tz)
return dt
def local_datetime(self):
"""Returns a local timezone-aware datetime object
It's the same as:
mayaDt.datetime(to_timezone=mayaDt.local_timezone)
"""
return self.datetime(to_timezone=self.local_timezone, naive=False)
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaDT."""
# Get a timezone-naive datetime.
dt = self.datetime(naive=True)
return "{}Z".format(dt.isoformat())
def rfc2822(self):
"""Returns an RFC 2822 representation of the MayaDT."""
return email.utils.formatdate(self.epoch, usegmt=True)
def rfc3339(self):
"""Returns an RFC 3339 representation of the MayaDT."""
return self.datetime().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-5] + "Z"
# Properties
# ----------
@property
def year(self):
return self.datetime().year
@property
def month(self):
return self.datetime().month
@property
def day(self):
return self.datetime().day
@property
def date(self):
return self.datetime().date()
@property
def week(self):
return self.datetime().isocalendar()[1]
@property
def weekday(self):
"""Return the day of the week as an integer.
Monday is 1 and Sunday is 7.
"""
return self.datetime().isoweekday()
@property
def hour(self):
return self.datetime().hour
@property
def minute(self):
return self.datetime().minute
@property
def second(self):
return self.datetime().second
@property
def microsecond(self):
return self.datetime().microsecond
@property
def epoch(self):
return int(self._epoch)
# Human Slang Extras
# ------------------
def slang_date(self, locale="en"):
""""Returns human slang representation of date.
Keyword Arguments:
locale -- locale to translate to, e.g. 'fr' for french.
(default: 'en' - English)
"""
dt = pendulum.instance(self.datetime())
try:
return _translate(dt, locale)
except KeyError:
pass
delta = humanize.time.abs_timedelta(
timedelta(seconds=(self.epoch - now().epoch))
)
format_string = "DD MMM"
if delta.days >= 365:
format_string += " YYYY"
return dt.format(format_string, locale=locale).title()
def slang_time(self, locale="en"):
""""Returns human slang representation of time.
Keyword Arguments:
locale -- locale to translate to, e.g. 'fr' for french.
(default: 'en' - English)
"""
dt = self.datetime()
return pendulum.instance(dt).diff_for_humans(locale=locale)
def utc_offset(time_struct=None):
"""
Returns the time offset from UTC accounting for DST
Keyword Arguments:
time_struct {time.struct_time} -- the struct time for which to
return the UTC offset.
If None, use current local time.
"""
if time_struct:
ts = time_struct
else:
ts = time.localtime()
if ts[-1]:
offset = time.altzone
else:
offset = time.timezone
return offset
def to_utc_offset_naive(dt):
if dt.tzinfo is None:
return dt
return dt.astimezone(pytz.utc).replace(tzinfo=None)
def to_utc_offset_aware(dt):
if dt.tzinfo is not None:
return dt
return pytz.utc.localize(dt)
def to_iso8601(dt):
return to_utc_offset_naive(dt).isoformat() + "Z"
def end_of_day_midnight(dt):
if dt.time() == time.min:
return dt
else:
return dt.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
@comparable
class MayaInterval(object):
"""
A MayaInterval represents a range between two datetimes,
inclusive of the start and exclusive of the end.
"""
def __init__(self, start=None, end=None, duration=None):
try:
# Ensure that proper arguments were passed.
assert any(
(
(start and end),
(start and duration is not None),
(end and duration is not None),
)
)
assert not all((start, end, duration is not None))
except AssertionError:
raise ValueError("Exactly 2 of start, end, and duration must be specified")
# Convert duration to timedelta if seconds were provided.
if duration:
duration = _seconds_or_timedelta(duration)
if not start:
start = end - duration
if not end:
end = start + duration
if start > end:
raise ValueError("MayaInterval cannot end before it starts")
self.start = start
self.end = end
def __repr__(self):
return "<MayaInterval start={0!r} end={1!r}>".format(self.start, self.end)
def iso8601(self):
"""Returns an ISO 8601 representation of the MayaInterval."""
return "{0}/{1}".format(self.start.iso8601(), self.end.iso8601())
@classmethod
def parse_iso8601_duration(cls, duration, start=None, end=None):
match = re.match(
r"(?:P(?P<weeks>\d+)W)|(?:P(?:(?:(?P<years>\d+)Y)?(?:(?P<months>\d+)M)?(?:(?P<days>\d+)D))?(?:T(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)?)", # noqa
duration,
)
time_components = {}
if match:
time_components = match.groupdict(0)
for key, value in time_components.items():
time_components[key] = int(value)
duration = relativedelta(**time_components)
if start:
return parse(start.datetime() + duration)
if end:
return parse(end.datetime() - duration)
return None
@classmethod
def from_iso8601(cls, s):
# # Start and end, such as "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z"
start, end = s.split("/")
try:
start = parse(start)
except pendulum.parsing.exceptions.ParserError:
# start = self._parse_iso8601_duration(start, end=end)
raise NotImplementedError()
try:
end = parse(end)
except (pendulum.parsing.exceptions.ParserError, TypeError):
end = cls.parse_iso8601_duration(end, start=start)
return cls(start=start, end=end)
# # Start and duration, such as "2007-03-01T13:00:00Z/P1Y2M10DT2H30M"
# # Duration and end, such as "P1Y2M10DT2H30M/2008-05-11T15:30:00Z"
@validate_arguments_type_of_function()
def __and__(self, maya_interval):
return self.intersection(maya_interval)
@validate_arguments_type_of_function()
def __or__(self, maya_interval):
return self.combine(maya_interval)
@validate_arguments_type_of_function()
def __eq__(self, maya_interval):
return self.start == maya_interval.start and self.end == maya_interval.end
def __hash__(self):
return hash((self.start, self.end))
def __iter__(self):
yield self.start
yield self.end
@validate_arguments_type_of_function()
def __cmp__(self, maya_interval):
return cmp(self.start, maya_interval.start) or cmp(self.end, maya_interval.end)
@property
def duration(self):
return self.timedelta.total_seconds()
@property
def timedelta(self):
return timedelta(seconds=(self.end.epoch - self.start.epoch))
@property
def is_instant(self):
return self.timedelta == timedelta(seconds=0)
def intersects(self, maya_interval):
return self & maya_interval is not None
@property
def midpoint(self):
return self.start.add(seconds=(self.duration / 2))
@validate_arguments_type_of_function()
def combine(self, maya_interval):
"""Returns a combined list of timespans, merged together."""
interval_list = sorted([self, maya_interval])
if self & maya_interval or self.is_adjacent(maya_interval):
return [
MayaInterval(
interval_list[0].start,
max(interval_list[0].end, interval_list[1].end),
)
]
return interval_list
@validate_arguments_type_of_function()
def subtract(self, maya_interval):
""""Removes the given interval."""
if not self & maya_interval:
return [self]
elif maya_interval.contains(self):
return []
interval_list = []
if self.start < maya_interval.start:
interval_list.append(MayaInterval(self.start, maya_interval.start))
if self.end > maya_interval.end:
interval_list.append(MayaInterval(maya_interval.end, self.end))
return interval_list
def split(self, duration, include_remainder=True):
# Convert seconds to timedelta, if appropriate.
duration = _seconds_or_timedelta(duration)
if duration <= timedelta(seconds=0):
raise ValueError("cannot call split with a non-positive timedelta")
start = self.start
while start < self.end:
if start + duration <= self.end:
yield MayaInterval(start, start + duration)
elif include_remainder:
yield MayaInterval(start, self.end)
start += duration
def quantize(self, duration, snap_out=False, timezone="UTC"):
"""Returns a quanitzed interval."""
# Convert seconds to timedelta, if appropriate.
duration = _seconds_or_timedelta(duration)
timezone = pytz.timezone(timezone)
if duration <= timedelta(seconds=0):
raise ValueError("cannot quantize by non-positive timedelta")
epoch = timezone.localize(Datetime(1970, 1, 1))
seconds = int(duration.total_seconds())
start_seconds = int((self.start.datetime(naive=False) - epoch).total_seconds())
end_seconds = int((self.end.datetime(naive=False) - epoch).total_seconds())
if start_seconds % seconds and not snap_out:
start_seconds += seconds
if end_seconds % seconds and snap_out:
end_seconds += seconds
start_seconds -= start_seconds % seconds
end_seconds -= end_seconds % seconds
if start_seconds > end_seconds:
start_seconds = end_seconds
return MayaInterval(
start=MayaDT.from_datetime(epoch).add(seconds=start_seconds),
end=MayaDT.from_datetime(epoch).add(seconds=end_seconds),
)
@validate_arguments_type_of_function()
def intersection(self, maya_interval):
"""Returns the intersection between two intervals."""
start = max(self.start, maya_interval.start)
end = min(self.end, maya_interval.end)
either_instant = self.is_instant or maya_interval.is_instant
instant_overlap = self.start == maya_interval.start or start <= end
if (either_instant and instant_overlap) or (start < end):
return MayaInterval(start, end)
@validate_arguments_type_of_function()
def contains(self, maya_interval):
return self.start <= maya_interval.start and self.end >= maya_interval.end
def __contains__(self, maya_dt):
if isinstance(maya_dt, MayaDT):
return self.contains_dt(maya_dt)
return self.contains(maya_dt)
def contains_dt(self, dt):
return self.start <= dt < self.end
@validate_arguments_type_of_function()
def is_adjacent(self, maya_interval):
return self.start == maya_interval.end or self.end == maya_interval.start
@property
def icalendar(self):
ical_dt_format = "%Y%m%dT%H%M%SZ"
return (
"""
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:{0}
DTEND:{1}
END:VEVENT
END:VCALENDAR
""".format(
self.start.datetime().strftime(ical_dt_format),
self.end.datetime().strftime(ical_dt_format),
)
.replace(" ", "")
.strip("\r\n")
.replace("\n", "\r\n")
)
@staticmethod
def flatten(interval_list):
return functools.reduce(
lambda reduced, maya_interval: (
(reduced[:-1] + maya_interval.combine(reduced[-1]))
if reduced
else [maya_interval]
),
sorted(interval_list),
[],
)
@classmethod
def from_datetime(cls, start_dt=None, end_dt=None, duration=None):
start = MayaDT.from_datetime(start_dt) if start_dt else None
end = MayaDT.from_datetime(end_dt) if end_dt else None
return cls(start=start, end=end, duration=duration)
def now():
"""Returns a MayaDT instance for this exact moment."""
epoch = time.time()
return MayaDT(epoch=epoch)
def when(string, timezone="UTC", prefer_dates_from="current_period"):
""""Returns a MayaDT instance for the human moment specified.
Powered by dateparser. Useful for scraping websites.
Examples:
'next week', 'now', 'tomorrow', '300 years ago', 'August 14, 2015'
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: 'UTC')
prefer_dates_from -- what dates are prefered when `string` is ambigous.
options are 'past', 'future', and 'current_period'
(default: 'current_period'). see: [1]
Reference:
[1] dateparser.readthedocs.io/en/latest/usage.html#handling-incomplete-dates
"""
settings = {
"TIMEZONE": timezone,
"RETURN_AS_TIMEZONE_AWARE": True,
"TO_TIMEZONE": "UTC",
"PREFER_DATES_FROM": prefer_dates_from,
}
dt = dateparser.parse(string, settings=settings)
if dt is None:
raise ValueError("invalid datetime input specified.")
return MayaDT.from_datetime(dt)
def parse(string, timezone="UTC", day_first=False, year_first=True, strict=False):
""""Returns a MayaDT instance for the machine-produced moment specified.
Powered by pendulum.
Accepts most known formats. Useful for working with data.
Keyword Arguments:
string -- string to be parsed
timezone -- timezone referenced from (default: 'UTC')
day_first -- if true, the first value (e.g. 01/05/2016)
is parsed as day.
if year_first is set to True, this distinguishes
between YDM and YMD. (default: False)
year_first -- if true, the first value (e.g. 2016/05/01)
is parsed as year (default: True)
strict -- if False, allow pendulum to fall back on datetime parsing
if pendulum's own parsing fails
"""
options = {}
options["tz"] = timezone
options["day_first"] = day_first
options["year_first"] = year_first
options["strict"] = strict
dt = pendulum.parse(str(string), **options)
return MayaDT.from_datetime(dt)
def _seconds_or_timedelta(duration):
"""Returns `datetime.timedelta` object for the passed duration.
Keyword Arguments:
duration -- `datetime.timedelta` object or seconds in `int` format.
"""
if isinstance(duration, int):
dt_timedelta = timedelta(seconds=duration)
elif isinstance(duration, timedelta):
dt_timedelta = duration
else:
raise TypeError(
"Expects argument as `datetime.timedelta` object "
"or seconds in `int` format"
)
return dt_timedelta
def _translate(dt, target_locale):
en = default_loader.get_locale("en")
target = default_loader.get_locale(target_locale)
naturaldate = humanize.naturaldate(dt)
base = en.translate(naturaldate, settings=dateparser.conf.settings)
return target.info["relative-type"][base][-1]
def intervals(start, end, interval):
"""
Yields MayaDT objects between the start and end MayaDTs given,
at a given interval (seconds or timedelta).
"""
interval = _seconds_or_timedelta(interval)
current_timestamp = start
while current_timestamp.epoch < end.epoch:
yield current_timestamp
current_timestamp = current_timestamp.add(seconds=interval.total_seconds())
Executable
+121
View File
@@ -0,0 +1,121 @@
import pytest
from datetime import datetime
import copy
import maya
def test_rfc2822():
r = maya.now().rfc2822()
d = maya.MayaDT.from_rfc2822(r)
assert r == d.rfc2822()
def test_iso8601():
r = maya.now().iso8601()
d = maya.MayaDT.from_iso8601(r)
assert r == d.iso8601()
def test_human_when():
r1 = maya.when('yesterday')
r2 = maya.when('today')
assert r2.day - r1.day == 1
def test_machine_parse():
r1 = maya.parse('August 14, 2015')
assert r1.day == 14
r2 = maya.parse('August 15, 2015')
assert r2.day == 15
def test_dt_tz_translation():
d1 = maya.now().datetime()
d2 = maya.now().datetime(to_timezone='US/Eastern')
assert d1.hour - d2.hour == 5
def test_dt_tz_naive():
d1 = maya.now().datetime(naive=True)
assert d1.tzinfo is None
d2 = maya.now().datetime(to_timezone='US/Eastern', naive=True)
assert d2.tzinfo is None
assert d1.hour - d2.hour == 5
def test_random_date():
d = maya.when('11-17-11 08:09:10')
assert d.year == 2011
assert d.month == 11
assert d.day == 17
assert d.hour == 8
assert d.minute == 9
assert d.second == 10
assert d.microsecond == 0
def test_print_date(capsys):
d = maya.when('11-17-11')
print(d)
out, err = capsys.readouterr()
assert out == '<MayaDT epoch=1321488000.0>\n'
def test_invalid_date():
with pytest.raises(ValueError):
maya.when('another day')
def test_slang_date():
d = maya.when('tomorrow')
assert d.slang_date() == 'tomorrow'
def test_slang_time():
d = maya.when('one hour ago')
assert d.slang_time() == 'an hour ago'
def test_parse():
d = maya.parse('February 21, 1994')
assert format(d) == '1994-02-21 00:00:00+00:00'
d = maya.parse('01/05/2016')
assert format(d) == '2016-01-05 00:00:00+00:00'
d = maya.parse('01/05/2016', day_first=True)
assert format(d) == '2016-05-01 00:00:00+00:00'
def test_datetime_to_timezone():
dt = maya.when('2016-01-01').datetime(to_timezone='US/Eastern')
assert dt.tzinfo.zone == 'US/Eastern'
def test_comparison_operations():
now = maya.now()
now_copy = copy.deepcopy(now)
tomorrow = maya.when('tomorrow')
assert (now == now_copy) is True
assert (now == tomorrow) is False
assert (now != now_copy) is False
assert (now != tomorrow) is True
assert (now < now_copy) is False
assert (now < tomorrow) is True
assert (now <= now_copy) is True
assert (now <= tomorrow) is True
assert (now > now_copy) is False
assert (now > tomorrow) is False
assert (now >= now_copy) is True
assert (now >= tomorrow) is False
View File
-25
View File
@@ -1,25 +0,0 @@
from freezegun import freeze_time
import pytest
@pytest.fixture(
params=[
("2018-03-25T00:00:00", 2),
("2018-03-25T01:00:00", 2),
("2018-03-25T02:00:00", 2),
("2018-03-25T02:30:00", 2),
("2018-03-25T03:00:00", 2),
("2018-03-25T04:00:00", 2),
("2018-10-28T00:00:00", 2),
("2018-10-28T01:00:00", 2),
("2018-10-28T02:00:00", 2),
("2018-10-28T02:30:00", 2),
("2018-10-28T03:00:00", 2),
("2018-10-28T04:00:00", 2),
],
ids=lambda x: x[0] + "_off_" + str(x[1]),
)
def frozen_now(request):
now_string, tz_offset = request.param
with freeze_time(now_string, tz_offset=tz_offset):
yield
-370
View File
@@ -1,370 +0,0 @@
import copy
import time
import calendar
from datetime import timedelta, datetime as Datetime
import pytz
import pytest
import maya
from maya.core import _seconds_or_timedelta # import private function
@pytest.mark.parametrize(
"string,expected", [("February 21, 1994", "Mon, 21 Feb 1994 00:00:00 GMT")]
)
def test_rfc2822(string, expected):
r = maya.parse(string).rfc2822()
d = maya.MayaDT.from_rfc2822(r)
assert r == expected
assert r == d.rfc2822()
@pytest.mark.parametrize(
"string,expected", [("February 21, 1994", "1994-02-21T00:00:00Z")]
)
def test_iso8601(string, expected):
r = maya.parse(string).iso8601()
d = maya.MayaDT.from_iso8601(r)
assert r == expected
assert r == d.iso8601()
@pytest.mark.parametrize(
"string,expected",
[
("20161001T1430.4+05:30", "2016-10-01T09:00:00.400000Z"),
("2016T14", "2016-01-01T14:00:00Z"),
("2016-10T14", "2016-10-01T14:00:00Z"),
("2012W05", "2012-01-30T00:00:00Z"),
("2012W055", "2012-02-03T00:00:00Z"),
("2012007", "2012-01-07T00:00:00Z"),
("2016-W07T09", "2016-02-15T09:00:00Z"),
],
)
def test_parse_iso8601(string, expected):
d = maya.MayaDT.from_iso8601(string)
assert expected == d.iso8601()
@pytest.mark.usefixtures("frozen_now")
def test_struct():
now = round(time.time())
ts = time.gmtime(now)
m = maya.MayaDT.from_struct(ts)
dt = Datetime.fromtimestamp(now, pytz.UTC)
assert m._epoch is not None
assert m.datetime() == dt
ts = time.localtime(now)
m = maya.MayaDT.from_struct(ts)
dt = Datetime.fromtimestamp(time.mktime(ts) - maya.core.utc_offset(ts), pytz.UTC)
assert m._epoch is not None
assert m.datetime() == dt
def test_issue_104():
e = 1507756331
t = Datetime.utcfromtimestamp(e)
t = maya.MayaDT.from_datetime(t)
assert str(t) == "Wed, 11 Oct 2017 21:12:11 GMT"
t = time.gmtime(e)
t = maya.MayaDT.from_struct(t)
assert str(t) == "Wed, 11 Oct 2017 21:12:11 GMT"
def test_human_when():
r1 = maya.when("yesterday")
r2 = maya.when("today")
assert (r2.day - r1.day) in (1, -30, -29, -28, -27)
def test_machine_parse():
r1 = maya.parse("August 14, 2015")
assert r1.day == 14
r2 = maya.parse("August 15, 2015")
assert r2.day == 15
@pytest.mark.usefixtures("frozen_now")
def test_dt_tz_translation():
d1 = maya.now().datetime()
d2 = maya.now().datetime(to_timezone="EST")
assert (d1.hour - d2.hour) % 24 == 5
@pytest.mark.usefixtures("frozen_now")
def test_dt_tz_naive():
d1 = maya.now().datetime(naive=True)
assert d1.tzinfo is None
d2 = maya.now().datetime(to_timezone="EST", naive=True)
assert d2.tzinfo is None
assert (d1.hour - d2.hour) % 24 == 5
def test_random_date():
# Test properties for maya.when()
d1 = maya.when("11-17-11 08:09:10")
assert d1.year == 2011
assert d1.month == 11
assert d1.day == 17
assert d1.week == 46
assert d1.weekday == 4
assert d1.hour == 8
assert d1.minute == 9
assert d1.second == 10
assert d1.microsecond == 0
# Test properties for maya.parse()
d2 = maya.parse("February 29, 1992 13:12:34")
assert d2.year == 1992
assert d2.month == 2
assert d2.day == 29
assert d2.week == 9
assert d2.weekday == 6
assert d2.hour == 13
assert d2.minute == 12
assert d2.second == 34
assert d2.microsecond == 0
def test_print_date(capsys):
d = maya.when("11-17-11")
print(d)
out, err = capsys.readouterr()
assert out == "Thu, 17 Nov 2011 00:00:00 GMT\n"
assert repr(d) == "<MayaDT epoch=1321488000.0>"
def test_invalid_date():
with pytest.raises(ValueError):
maya.when("another day")
def test_slang_date():
d = maya.when("tomorrow")
assert d.slang_date() == "tomorrow"
def test_slang_date_locale():
d = maya.when("tomorrow")
assert d.slang_date(locale="fr") == "demain"
def test_slang_time():
d = maya.when("1 hour ago")
assert d.slang_time() == "1 hour ago"
def test_slang_time_locale():
d = maya.when("1 hour ago")
assert d.slang_time(locale="de") == "vor 1 Stunde"
@pytest.mark.parametrize(
"string,kwds,expected",
[
("February 21, 1994", {}, "1994-02-21 00:00:00+00:00"),
("01/05/2016", {}, "2016-01-05 00:00:00+00:00"),
("01/05/2016", dict(day_first=True), "2016-05-01 00:00:00+00:00"),
(
"2016/05/01",
dict(year_first=True, day_first=False),
"2016-05-01 00:00:00+00:00",
),
(
"2016/01/05",
dict(year_first=True, day_first=True),
"2016-05-01 00:00:00+00:00",
),
("01/05/2016", dict(timezone="UTC"), "2016-01-05 00:00:00+00:00"),
("01/05/2016", dict(timezone="US/Central"), "2016-01-05 06:00:00+00:00"),
],
)
def test_parse(string, kwds, expected):
d = maya.parse(string, **kwds)
assert format(d) == expected
@pytest.mark.usefixtures("frozen_now")
def test_when_past():
two_days_away = maya.now().add(days=2)
past_date = maya.when(two_days_away.slang_date(), prefer_dates_from="past")
assert past_date < maya.now()
@pytest.mark.usefixtures("frozen_now")
def test_when_future():
two_days_away = maya.now().add(days=2)
future_date = maya.when(two_days_away.slang_date(), prefer_dates_from="future")
assert future_date > maya.now()
@pytest.mark.usefixtures("frozen_now")
def test_when_past_day_name():
two_days_away = maya.now().add(days=2)
past_date = maya.when(
calendar.day_name[two_days_away.weekday], prefer_dates_from="past"
)
assert past_date < maya.now()
@pytest.mark.usefixtures("frozen_now")
def test_when_future_day_name():
two_days_away = maya.now().add(days=2)
future_date = maya.when(
calendar.day_name[two_days_away.weekday], prefer_dates_from="future"
)
assert future_date > maya.now()
def test_datetime_to_timezone():
dt = maya.when("2016-01-01").datetime(to_timezone="US/Eastern")
assert dt.tzinfo.zone == "US/Eastern"
def test_rfc3339_epoch():
mdt = maya.when("2016-01-01")
out = mdt.rfc3339()
mdt2 = maya.MayaDT.from_rfc3339(out)
assert mdt.epoch == mdt2.epoch
def test_rfc3339_format():
rfc3339 = maya.MayaDT.rfc3339(maya.when("2016-01-01T12:03:03Z"))
# it's important that the string has got a "max 1-digit millis" fragment
# as per https://tools.ietf.org/html/rfc3339#section-5.6
assert rfc3339 == "2016-01-01T12:03:03.0Z"
@pytest.mark.usefixtures("frozen_now")
def test_comparison_operations():
now = maya.now()
now_copy = copy.deepcopy(now)
tomorrow = maya.when("tomorrow")
assert (now == now_copy) is True
assert (now == tomorrow) is False
assert (now != now_copy) is False
assert (now != tomorrow) is True
assert (now < now_copy) is False
assert (now < tomorrow) is True
assert (now <= now_copy) is True
assert (now <= tomorrow) is True
assert (now > now_copy) is False
assert (now > tomorrow) is False
assert (now >= now_copy) is True
assert (now >= tomorrow) is False
# Check Exceptions
with pytest.raises(TypeError):
now == 1
with pytest.raises(TypeError):
now != 1
with pytest.raises(TypeError):
now < 1
with pytest.raises(TypeError):
now <= 1
with pytest.raises(TypeError):
now > 1
with pytest.raises(TypeError):
now >= 1
def test_seconds_or_timedelta():
# test for value in seconds
assert _seconds_or_timedelta(1234) == timedelta(0, 1234)
# test for value as `datetime.timedelta`
assert _seconds_or_timedelta(timedelta(0, 1234)) == timedelta(0, 1234)
# test for invalid value
with pytest.raises(TypeError):
_seconds_or_timedelta("invalid interval")
@pytest.mark.usefixtures("frozen_now")
def test_intervals():
now = maya.now()
tomorrow = now.add(days=1)
assert len(list(maya.intervals(now, tomorrow, 60 * 60))) == 24
@pytest.mark.usefixtures("frozen_now")
def test_dunder_add():
now = maya.now()
assert now + 1 == now.add(seconds=1)
assert now + timedelta(seconds=1) == now.add(seconds=1)
@pytest.mark.usefixtures("frozen_now")
def test_dunder_radd():
now = maya.now()
assert now.add(seconds=1) == now + 1
assert now.add(seconds=1) == now + timedelta(seconds=1)
@pytest.mark.usefixtures("frozen_now")
def test_dunder_sub():
now = maya.now()
assert now - 1 == now.subtract(seconds=1)
assert now - timedelta(seconds=1) == now.subtract(seconds=1)
@pytest.mark.usefixtures("frozen_now")
def test_mayaDT_sub():
now = maya.now()
then = now.add(days=1)
assert then - now == timedelta(seconds=24 * 60 * 60)
assert now - then == timedelta(seconds=-24 * 60 * 60)
def test_core_local_timezone(monkeypatch):
@property
def mock_local_tz(self):
class StaticTzInfo(object):
zone = "local"
def __repr__(self):
return "<StaticTzInfo 'local'>"
return StaticTzInfo()
monkeypatch.setattr(maya.MayaDT, "_local_tz", mock_local_tz)
mdt = maya.MayaDT(0)
assert mdt.local_timezone == "UTC"
def test_getting_datetime_for_local_timezone(monkeypatch):
@property
def mock_local_tz(self):
class StaticTzInfo(object):
zone = "Europe/Zurich"
def __repr__(self):
return "<StaticTzInfo 'Europe/Zurich'>"
return StaticTzInfo()
monkeypatch.setattr(maya.MayaDT, "_local_tz", mock_local_tz)
d = maya.parse("1994-02-21T12:00:00+05:30")
dt = pytz.timezone("Europe/Zurich").localize(Datetime(1994, 2, 21, 7, 30))
assert d.local_datetime() == dt
@pytest.mark.parametrize(
"when_str,snap_str,expected_when",
[("Mon, 21 Feb 1994 21:21:42 GMT", "@d", "Mon, 21 Feb 1994 00:00:00 GMT")],
)
def test_snaptime(when_str, snap_str, expected_when):
# given
dt = maya.when(when_str)
# when
dt = dt.snap(snap_str)
# then
assert dt == maya.when(expected_when)
-561
View File
@@ -1,561 +0,0 @@
import random
from datetime import datetime, timedelta
import pytest
import pytz
import maya
from maya.compat import cmp
Los_Angeles = pytz.timezone("America/Los_Angeles")
New_York = pytz.timezone("America/New_York")
Melbourne = pytz.timezone("Australia/Melbourne")
def test_interval_requires_2_of_start_end_duration():
start = maya.now()
end = start.add(hours=1)
with pytest.raises(ValueError):
maya.MayaInterval(start=start)
with pytest.raises(ValueError):
maya.MayaInterval(end=end)
with pytest.raises(ValueError):
maya.MayaInterval(duration=60)
with pytest.raises(ValueError):
maya.MayaInterval(start=start, end=end, duration=60)
maya.MayaInterval(start=start, end=end)
maya.MayaInterval(start=start, duration=60)
maya.MayaInterval(end=end, duration=60)
def test_interval_requires_end_time_after_or_on_start_time():
with pytest.raises(ValueError):
maya.MayaInterval(start=maya.now(), duration=0)
maya.MayaInterval(start=maya.now(), duration=-1)
def test_interval_init_start_end():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert interval.start == start
assert interval.end == end
def test_interval_init_start_duration():
start = maya.now()
duration = 1
interval = maya.MayaInterval(start=start, duration=duration)
assert interval.start == start
assert interval.end == start.add(seconds=duration)
def test_interval_init_end_duration():
end = maya.now()
duration = 1
interval = maya.MayaInterval(end=end, duration=duration)
assert interval.end == end
assert interval.start == end.subtract(seconds=duration)
@pytest.mark.parametrize(
"start_doy1,end_doy1,start_doy2,end_doy2,intersection_doys",
(
(0, 2, 1, 3, (1, 2)),
(0, 2, 3, 4, None),
(0, 2, 2, 3, None),
(0, 1, 0, 1, (0, 1)),
(1, 1, 1, 3, (1, 1)),
(1, 1, 1, 1, (1, 1)),
(1, 1, 2, 3, None),
(2, 2, 1, 3, (2, 2)),
(1, 3, 1, 1, (1, 1)),
(2, 3, 1, 1, None),
(1, 3, 2, 2, (2, 2)),
),
ids=(
"overlapping",
"non-overlapping",
"adjacent",
"equal",
"instant overlapping start only",
"instant equal",
"instant disjoint",
"instant overlapping",
"instant overlapping start only (left)",
"instant disjoint (left)",
"instant overlapping (left)",
),
)
def test_interval_intersection(
start_doy1, end_doy1, start_doy2, end_doy2, intersection_doys
):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(base.add(days=start_doy1), base.add(days=end_doy1))
interval2 = maya.MayaInterval(base.add(days=start_doy2), base.add(days=end_doy2))
if intersection_doys:
start_doy_intersection, end_doy_intersection = intersection_doys
assert interval1 & interval2 == maya.MayaInterval(
base.add(days=start_doy_intersection), base.add(days=end_doy_intersection)
)
else:
assert (interval1 & interval2) is None
# check invalid argument
with pytest.raises(TypeError):
interval1 & "invalid type"
def test_interval_intersects():
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval = maya.MayaInterval(base, base.add(days=1))
assert interval.intersects(interval)
assert not interval.intersects(
maya.MayaInterval(base.add(days=2), base.add(days=3))
)
# check invalid argument
with pytest.raises(TypeError):
interval.intersects("invalid type")
def test_and_operator():
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(base, base.add(days=2))
interval2 = maya.MayaInterval(base.add(days=1), base.add(days=3))
assert (
interval1 & interval2
== interval2 & interval1 # noqa
== interval1.intersection(interval2) # noqa
)
# check invalid argument
with pytest.raises(TypeError):
interval1.intersection("invalid type")
def test_interval_eq_operator():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert interval == maya.MayaInterval(start=start, end=end)
assert interval != maya.MayaInterval(start=start, end=end.add(days=1))
# check invalid argument
with pytest.raises(TypeError):
interval == "invalid type"
with pytest.raises(TypeError):
interval != "invalid type"
def test_interval_timedelta():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.timedelta == delta
def test_interval_duration():
start = maya.now()
delta = timedelta(hours=1)
interval = maya.MayaInterval(start=start, duration=delta)
assert interval.duration == delta.total_seconds()
@pytest.mark.parametrize(
"start_doy1,end_doy1,start_doy2,end_doy2,expected",
(
(0, 2, 1, 3, False),
(0, 2, 3, 4, False),
(0, 2, 2, 3, False),
(0, 1, 0, 1, True),
(0, 3, 1, 2, True),
),
ids=("overlapping", "non-overlapping", "adjacent", "equal", "subset"),
)
def test_interval_contains(start_doy1, end_doy1, start_doy2, end_doy2, expected):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval1 = maya.MayaInterval(base.add(days=start_doy1), base.add(days=end_doy1))
interval2 = maya.MayaInterval(base.add(days=start_doy2), base.add(days=end_doy2))
assert interval1.contains(interval2) is expected
assert (interval2 in interval1) is expected
# check invalid argument
with pytest.raises(TypeError):
interval1.contains("invalid type")
@pytest.mark.parametrize(
"start_doy,end_doy,dt_doy,expected",
(
(2, 4, 1, False),
(2, 4, 2, True),
(2, 4, 3, True),
(2, 4, 4, False),
(2, 4, 5, False),
),
ids=("before-start", "on-start", "during", "on-end", "after-end"),
)
def test_interval_in_operator_maya_dt(start_doy, end_doy, dt_doy, expected):
base = maya.MayaDT.from_datetime(datetime(2016, 1, 1))
interval = maya.MayaInterval(
start=base.add(days=start_doy), end=base.add(days=end_doy)
)
dt = base.add(days=dt_doy)
assert (dt in interval) is expected
# check invalid argument
with pytest.raises(TypeError):
"invalid type" in interval
def test_interval_hash():
start = maya.now()
end = start.add(hours=1)
interval = maya.MayaInterval(start=start, end=end)
assert hash(interval) == hash(maya.MayaInterval(start=start, end=end))
assert hash(interval) != hash(maya.MayaInterval(start=start, end=end.add(days=1)))
def test_interval_iter():
start = maya.now()
end = start.add(days=1)
assert tuple(maya.MayaInterval(start=start, end=end)) == (start, end)
@pytest.mark.parametrize(
"start1,end1,start2,end2,expected",
[(1, 2, 1, 2, 0), (1, 3, 2, 4, -1), (2, 4, 1, 3, 1), (1, 2, 1, 3, -1)],
ids=("equal", "less-than", "greater-than", "use-end-time-if-start-time-identical"),
)
def test_interval_cmp(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(start=base.add(days=start1), end=base.add(days=end1))
interval2 = maya.MayaInterval(start=base.add(days=start2), end=base.add(days=end2))
assert cmp(interval1, interval2) == expected
# check invalid argument
with pytest.raises(TypeError):
cmp(interval1, "invalid type")
@pytest.mark.parametrize(
"start1,end1,start2,end2,expected",
[
(1, 2, 2, 3, [(1, 3)]),
(1, 3, 2, 4, [(1, 4)]),
(1, 2, 3, 4, [(1, 2), (3, 4)]),
(1, 5, 2, 3, [(1, 5)]),
],
ids=("adjacent", "overlapping", "non-overlapping", "contains"),
)
def test_interval_combine(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(start=base.add(days=start1), end=base.add(days=end1))
interval2 = maya.MayaInterval(start=base.add(days=start2), end=base.add(days=end2))
expected_intervals = [
maya.MayaInterval(start=base.add(days=start), end=base.add(days=end))
for start, end in expected
]
assert interval1.combine(interval2) == expected_intervals
assert interval2.combine(interval1) == expected_intervals
# check invalid argument
with pytest.raises(TypeError):
interval2.combine("invalid type")
@pytest.mark.parametrize(
"start1,end1,start2,end2,expected",
[
(1, 2, 3, 4, [(1, 2)]),
(1, 2, 2, 4, [(1, 2)]),
(2, 3, 1, 4, []),
(1, 4, 2, 3, [(1, 2), (3, 4)]),
(1, 4, 0, 2, [(2, 4)]),
(1, 4, 3, 5, [(1, 3)]),
(1, 4, 1, 2, [(2, 4)]),
(1, 4, 3, 4, [(1, 3)]),
],
ids=(
"non-overlapping",
"adjacent",
"contains",
"splits",
"overlaps-left",
"overlaps-right",
"overlaps-left-identical-start",
"overlaps-right-identical-end",
),
)
def test_interval_subtract(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(start=base.add(days=start1), end=base.add(days=end1))
interval2 = maya.MayaInterval(start=base.add(days=start2), end=base.add(days=end2))
expected_intervals = [
maya.MayaInterval(start=base.add(days=start), end=base.add(days=end))
for start, end in expected
]
assert interval1.subtract(interval2) == expected_intervals
# check invalid argument
with pytest.raises(TypeError):
interval1.subtract("invalid type")
@pytest.mark.parametrize(
"start1,end1,start2,end2,expected",
[(1, 2, 2, 3, True), (2, 3, 1, 2, True), (1, 3, 2, 3, False), (2, 3, 4, 5, False)],
ids=("adjacent-right", "adjacent-left", "overlapping", "non-overlapping"),
)
def test_interval_is_adjacent(start1, end1, start2, end2, expected):
base = maya.now()
interval1 = maya.MayaInterval(start=base.add(days=start1), end=base.add(days=end1))
interval2 = maya.MayaInterval(start=base.add(days=start2), end=base.add(days=end2))
assert interval1.is_adjacent(interval2) == expected
# check invalid argument
with pytest.raises(TypeError):
interval1.is_adjacent("invalid type")
@pytest.mark.parametrize(
"start,end,delta,include_remainder,expected",
[
(0, 10, 5, False, [(0, 5), (5, 10)]),
(0, 10, 5, True, [(0, 5), (5, 10)]),
(0, 10, 3, False, [(0, 3), (3, 6), (6, 9)]),
(0, 10, 3, True, [(0, 3), (3, 6), (6, 9), (9, 10)]),
(0, 2, 5, False, []),
(0, 2, 5, True, [(0, 2)]),
],
ids=(
"even-split",
"even-split-include-partial",
"uneven-split-do-not-include-partial",
"uneven-split-include-partial",
"delta-larger-than-timepsan-do-not-include-partial",
"delta-larger-than-timepsan-include-partial",
),
)
def test_interval_split(start, end, delta, include_remainder, expected):
base = maya.now()
interval = maya.MayaInterval(start=base.add(days=start), end=base.add(days=end))
delta = timedelta(days=delta)
expected_intervals = [
maya.MayaInterval(start=base.add(days=s), end=base.add(days=e))
for s, e in expected
]
assert expected_intervals == list(
interval.split(delta, include_remainder=include_remainder)
)
def test_interval_split_non_positive_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(ValueError):
list(interval.split(timedelta(seconds=0)))
with pytest.raises(ValueError):
list(interval.split(timedelta(seconds=-10)))
@pytest.mark.parametrize(
"start,end,minutes,timezone,snap_out,expected_start,expected_end",
[
((5, 12), (8, 48), 30, None, False, (5, 30), (8, 30)),
((5, 12), (8, 48), 30, None, True, (5, 0), (9, 0)),
((5, 15), (9, 0), 15, None, False, (5, 15), (9, 0)),
((5, 15), (9, 0), 15, None, True, (5, 15), (9, 0)),
((6, 50), (9, 15), 60, "America/New_York", False, (7, 0), (9, 0)),
((6, 50), (9, 15), 60, "America/New_York", True, (6, 0), (10, 0)),
((6, 20), (6, 50), 60, None, False, (6, 0), (6, 0)),
((6, 20), (6, 50), 60, None, True, (6, 0), (7, 0)),
((6, 20), (6, 50), 60, "America/Chicago", False, (6, 0), (6, 0)),
((6, 20), (6, 50), 60, "America/Chicago", True, (6, 0), (7, 0)),
],
ids=(
"normal",
"normal-snap_out",
"already-quantized",
"already-quantized-snap_out",
"with-timezone",
"with-timezone-snap_out",
"too-small",
"too-small-snap_out",
"too-small-with-timezone",
"too-small-with-timezone-snap_out",
),
)
def test_quantize(
start, end, minutes, timezone, snap_out, expected_start, expected_end
):
base = maya.MayaDT.from_datetime(datetime(2017, 1, 1))
interval = maya.MayaInterval(
start=base.add(hours=start[0], minutes=start[1]),
end=base.add(hours=end[0], minutes=end[1]),
)
kwargs = {"timezone": timezone} if timezone is not None else {}
quantized_interval = interval.quantize(
timedelta(minutes=minutes), snap_out=snap_out, **kwargs
)
assert quantized_interval == maya.MayaInterval(
start=base.add(hours=expected_start[0], minutes=expected_start[1]),
end=base.add(hours=expected_end[0], minutes=expected_end[1]),
)
def test_quantize_invalid_delta():
start = maya.now()
end = start.add(days=1)
interval = maya.MayaInterval(start=start, end=end)
with pytest.raises(ValueError):
interval.quantize(timedelta(minutes=0))
with pytest.raises(ValueError):
interval.quantize(timedelta(minutes=-1))
def test_interval_flatten_non_overlapping():
step = 2
max_hour = 20
base = maya.now()
intervals = [
maya.MayaInterval(
start=base.add(hours=hour), duration=timedelta(hours=step - 1)
)
for hour in range(0, max_hour, step)
]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == sorted(intervals)
def test_interval_flatten_adjacent():
step = 2
max_hour = 20
base = maya.when("jan/1/2011")
intervals = [
maya.MayaInterval(start=base.add(hours=hour), duration=timedelta(hours=step))
for hour in range(0, max_hour, step)
]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [
maya.MayaInterval(start=base, duration=timedelta(hours=max_hour))
]
def test_interval_flatten_intersecting():
step = 2
max_hour = 20
base = maya.now()
intervals = [
maya.MayaInterval(
start=base.add(hours=hour), duration=timedelta(hours=step, minutes=30)
)
for hour in range(0, max_hour, step)
]
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [
maya.MayaInterval(start=base, duration=timedelta(hours=max_hour, minutes=30))
]
def test_interval_flatten_containing():
step = 2
max_hour = 20
base = maya.now()
containing_interval = maya.MayaInterval(
start=base, end=base.add(hours=max_hour + step)
)
intervals = [
maya.MayaInterval(
start=base.add(hours=hour), duration=timedelta(hours=step - 1)
)
for hour in range(2, max_hour, step)
]
intervals.append(containing_interval)
random.shuffle(intervals)
assert maya.MayaInterval.flatten(intervals) == [containing_interval]
def test_interval_from_datetime():
start = maya.now()
duration = timedelta(hours=1)
end = start + duration
interval = maya.MayaInterval.from_datetime(
start_dt=start.datetime(naive=False), end_dt=end.datetime(naive=False)
)
assert interval.start == start
assert interval.end == end
interval2 = maya.MayaInterval.from_datetime(
start_dt=start.datetime(naive=False), duration=duration
)
assert interval2.start == start
assert interval2.end == end
interval3 = maya.MayaInterval.from_datetime(
end_dt=end.datetime(naive=False), duration=duration
)
assert interval3.start == start
assert interval3.end == end
def test_interval_iso8601():
start = maya.when("11-17-11 08:09:10")
interval = maya.MayaInterval(start=start, duration=1)
assert interval.iso8601() == "2011-11-17T08:09:10Z/2011-11-17T08:09:11Z"
def test_interval_from_iso8601():
interval = maya.MayaInterval.from_iso8601(
"2018-03-18T14:27:18Z/2018-04-01T04:15:27Z"
)
s = maya.when("2018-03-18T14:27:18Z")
e = maya.when("2018-04-01T04:15:27Z")
assert interval.start == s
assert interval.end == e
def test_interval_from_iso8601_duration():
interval = maya.MayaInterval.from_iso8601("2018-03-18T14:27:18Z/P13DT13H48M9S")
s = maya.when("2018-03-18T14:27:18Z")
e = maya.when("2018-04-01T04:15:27Z")
assert interval.start == s
assert interval.end == e
interval = maya.MayaInterval.from_iso8601("2018-03-05T14:27:18Z/P2W")
s = maya.when("2018-03-05T14:27:18Z")
e = maya.when("2018-03-19T14:27:18Z")
assert interval.start == s
assert interval.end == e
@pytest.mark.parametrize(
"start_string,end_string,interval,expected_count",
[
("2019-01-03 11:40:00Z", "2019-01-03 11:40:20Z", 2, 10),
("2019-01-03 11:40:00Z", "2019-01-03 11:40:30Z", timedelta(seconds=2), 15),
("2019-01-03 11:40:00Z", "2019-01-03 11:45:00Z", 2 * 60, 3),
("2019-01-03 11:40:00Z", "2019-01-03 11:51:00Z", timedelta(minutes=1), 11),
("2019-01-03 11:40:00Z", "2019-01-03 21:40:00Z", 3 * 60 * 60, 4),
("2019-01-03 11:40:00Z", "2019-01-03 13:41:00Z", timedelta(hours=1), 3),
("2019-01-03 11:40:00Z", "2019-01-09 11:40:00Z", 3 * 60 * 60 * 24, 2),
("2019-01-03 11:40:00Z", "2019-01-05 12:00:00Z", timedelta(days=2), 2),
],
ids=(
"seconds",
"seconds-timedelta",
"minutes",
"minutes-timedelta",
"hours",
"hours-timedelta",
"days",
"days-timedelta",
),
)
def test_intervals(start_string, end_string, interval, expected_count):
start = maya.parse(start_string)
end = maya.parse(end_string)
assert len(list(maya.intervals(start, end, interval))) == expected_count
def test_issue_168_regression():
start = maya.now()
end = start.add(weeks=1)
gen = maya.intervals(start=start, end=end, interval=60 * 60 * 24)
# Since the bug causes the generator to never end, first sanity
# check that two results are not the same.
assert next(gen) != next(gen)
assert len(list(maya.intervals(start=start, end=end, interval=60 * 60 * 24))) == 7
-43
View File
@@ -1,43 +0,0 @@
[tox]
envlist = lint,manifest,py27,py35,py36,py37,docs,coverage-report
[testenv]
# Prevent random setuptools/pip breakages like
# https://github.com/pypa/setuptools/issues/1042 from breaking our builds.
setenv =
VIRTUALENV_NO_DOWNLOAD=1
extras = {env:TOX_AP_TEST_EXTRAS:tests}
commands = coverage run --parallel -m pytest {posargs}
[testenv:coverage-report]
basepython = python3.7
skip_install = true
deps = coverage
commands =
coverage combine
coverage report
[testenv:lint]
basepython = python3.7
skip_install = true
deps = pre-commit
passenv = HOMEPATH # needed on Windows
commands = pre-commit run --all-files
[testenv:docs]
basepython = python3.7
extras = docs
commands =
sphinx-build -W -b html -d {envtmpdir}/doctrees docs/source docs/_build/html
;sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs/source docs/_build/html
[testenv:manifest]
basepython = python3.7
deps = check-manifest
skip_install = true
commands = check-manifest