From 1cd9d55b73150ffd691847f46a7837a0f8003022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81bastien=20Eustace?= Date: Mon, 27 Jun 2016 18:50:31 -0500 Subject: [PATCH] Initial commit --- .gitignore | 23 + .travis.yml | 10 + CHANGELOG.md | 252 +++++ LICENSE | 20 + MANIFEST.in | 3 + Makefile | 26 + README.rst | 8 + docs/Makefile | 225 ++++ docs/_static/theme_overrides.css | 1071 ++++++++++++++++++++ docs/conf.py | 333 ++++++ docs/index.rst | 22 + pendulum/__init__.py | 3 + pendulum/_compat.py | 7 + pendulum/exceptions.py | 6 + pendulum/pendulum.py | 1463 +++++++++++++++++++++++++++ pendulum/utils.py | 72 ++ pendulum/version.py | 3 + requirements.txt | 4 + setup.py | 47 + tests-requirements.txt | 2 + tests/__init__.py | 38 + tests/test_construct.py | 75 ++ tests/test_create_from_date.py | 42 + tests/test_create_from_format.py | 33 + tests/test_create_from_time.py | 47 + tests/test_create_from_timestamp.py | 24 + tests/test_day_of_week_modifiers.py | 211 ++++ tests/test_strings.py | 106 ++ tests/test_timezone.py | 28 + tox.ini | 12 + 30 files changed, 4216 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.rst create mode 100644 docs/Makefile create mode 100644 docs/_static/theme_overrides.css create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 pendulum/__init__.py create mode 100644 pendulum/_compat.py create mode 100644 pendulum/exceptions.py create mode 100644 pendulum/pendulum.py create mode 100644 pendulum/utils.py create mode 100644 pendulum/version.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests-requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_construct.py create mode 100644 tests/test_create_from_date.py create mode 100644 tests/test_create_from_format.py create mode 100644 tests/test_create_from_time.py create mode 100644 tests/test_create_from_timestamp.py create mode 100644 tests/test_day_of_week_modifiers.py create mode 100644 tests/test_strings.py create mode 100644 tests/test_timezone.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..17379231 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +*.pyc + +# Packages +*.egg +*.egg-info +dist +build +_build +.cache + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +.DS_Store +.idea/* + +/test.py +/test_*.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..c5a849b2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python + +python: + - "2.7" + - "3.5" + +install: + - pip install -r tests-requirements.txt + +script: py.test tests/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..cdd610c9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,252 @@ +## 0.8.2 + +##### Fixes + +###### ORM + +- Fixing a possible `Memory Error: stack overflow` error when accessing relations. +- Fixing builder copying process to avoir issues with `PyMySQL`(thanks to [ihumanable](https://github.com/ihumanable)). + +###### Commands + +- Fixing the `-n/--no-interaction` option not automatically confirming questions. +- Removing the check character in migration commands output to avoid errors on Windows. + +###### Connection + +- Updating connectors to raise an exception when backend packages are missing. +- Adding standard name resolution to the `purge` method (thanks to [ihumanable](https://github.com/ihumanable)). + +###### DBAL + +- Fixing setting foreign key constraint name for MySQL. +- Handling missing `constraint_name` for sqlite (thanks to [ihumanable](https://github.com/ihumanable)). + + +## 0.8.1 + +##### Fixes + +###### ORM + +- Removing call to `Model._boot_columns()` to avoid errors for column types not supported by the dbal. + +###### Schema Builder + +- Fixing `Blueprint.char()` method (thanks to [ihumanable](https://github.com/ihumanable)). +- Fixing `Fluent` behavior. + +###### Commands + +- Fixing `orator` command not working on Windows. +- Fixing `migrate:status` command not switching databases. + +###### Connection + +- Fixing a bug when calling `Connnection.disconnect()` after a reconnection when not using read/write connections. +- Fixing `MySQLConnection.get_server_version()` method to be compatible with `PyMySQL` (thanks to [gdraynz](https://github.com/gdraynz)). + +## 0.8 + +##### Improvements + +###### ORM + +- [#30](https://github.com/sdispater/orator/issues/30) Support for default values +- [#29](https://github.com/sdispater/orator/issues/29) Supporting only one timetamp column on models +- [#26](https://github.com/sdispater/orator/issues/26) Adding support for extra conditions on relationships +- Adding `@scope` decorator to define query scopes. +- Improving global scopes + +###### Schema builder + +- Adding support for a `use_current()` on timestamps +- Improving dbal to support SQLite fully. +- Improving fluents + +###### Query Builder + +- [#28](https://github.com/sdispater/orator/issues/28) Making where_in() method accept Collection instances + +###### Commands + +- Adding a `make:model` command + +###### Connection + +- Using unicode by default for mysql and postgres. +- Improves how queries are run in `Connection` class + +###### Collections + +- Adds `flatten()` method to `Collection` class + +##### Fixes + +###### ORM + +- Fixes `Model.get_foreign_key()` method +- Fixes soft deletes +- Avoid going through \_\_setattr\_\_ method when setting timestamps + +###### Schema Builder + +- [#33](https://github.com/sdispater/orator/issues/33) [SQLite] Renaming or dropping columns loses NULL constraint +- [#32](https://github.com/sdispater/orator/issues/32) [SQLite] Renaming or dropping columns fails when columns' name is a keyword +- [#31](https://github.com/sdispater/orator/issues/31) [SQLite] Changing columns loses default column values. + +###### Query Builder + +- Fixes query grammar default columns value + +###### Connection + +- Fixing `Connection._try_again_if_caused_by_lost_connection()` not being called +- Preventing default connection being set to None +- Fixing json type behavior for Postgres + +###### Migrations +- Fixing migration stubs + + +### 0.7.1 + +(November 30th, 2015) + +##### Improvements + +- [#20](https://github.com/sdispater/orator/issues/20) Collections have been improved (New methods added) +- Commands have been improved +- The `to_dict` methods on the `Model`, `Collection` classes and paginators are now deprecated. Use `serialize` instead. + +##### Fixes + +* [#22](https://github.com/sdispater/orator/issues/22) Model.fill() and other methods now accept a dictionary in addition to keyword arguments. +* MySQL charset config value was not used when connecting. This is now fixed. (Thanks to [@heavenshell](https://github.com/heavenshell)) +* [#24](https://github.com/sdispater/orator/issues/24) Dynamic properties called the wrong methods when accessing the related items. + + +### 0.7 + +(November 10th, 2015) + +##### Improvements + +- [#15](https://github.com/sdispater/orator/issues/9) Execute migrations inside a transaction. +- [#13](https://github.com/sdispater/orator/issues/9) Support database seeding and model factories. +- [#9](https://github.com/sdispater/orator/issues/9) Support for SQLite foreign keys. +- Relationships decorators. +- Morph relationships now using a name (default being the table name) rather than a class name. + +##### Fixes + +- [#14](https://github.com/sdispater/orator/issues/14) Changing columns with SchemaBuilder does not work with some types. +- [#16](https://github.com/sdispater/orator/issues/16) The last page of LengthAwarePaginator is not calculated properly in Python 2.7. +- Avoid an error when psycopg2 is not installed. +- Fix dynamic properties for eagerloaded relationships. + + +### 0.6.4 + +(July 7th, 2015) + +##### Fixes + +- [#11](https://github.com/sdispater/orator/issues/11) Paginator.resolve_current_page() raises and error on Python 2.7. + + +### 0.6.3 + +(June 30th, 2015) + +##### Improvements + +- [#10](https://github.com/sdispater/orator/issues/10) Remove hard dependencies in commands. + +##### Fixes + +- [#8](https://github.com/sdispater/orator/issues/8) Reconnection on lost connection does not properly work. + + +### 0.6.2 + +(June 9nd, 2015) + +##### Fixes + +- Fixes a bug when results rather than the relation was returned +- Starting a new query from a BelongsToMany relation does not maintain pivot columns. +- Model.set_table() method does not properly handle pivot classes. +- Model.fresh() method raises an error for models retrieved from relations. + + +### 0.6.1 + +(June 2nd, 2015) + +- Fixes a lot of problems that broke relations behavior in 0.6. +- Adds raw() method to orm builder passthru. + +### 0.6 + +(May 31th, 2015) + +- Adds pagination support +- Adds model events support +- Implements Model.load() method +- Adds to_json() method to collections +- Makes to_json() methods consistent. +- Fixes how relations are retrieved from strings +- Fixes classes lookup in morph_to() method +- Fixes mutators not being called when initiating models +- Improves models attributes lookup +- Removes DynamicProperty class. Relations are dynamic themselves. + +### 0.5 + +(May 24th, 2015) + +- Adds database migrations +- Adds mutators and accessors +- Fix BelongsToMany.save_many() default joinings value + +### 0.4 + +(April 28th, 2015) + +- Adds Schema Builder +- Adds scopes support +- Adds support for related name in relationships declaration + +### 0.3.1 + +(April 19th, 2015) + +- Fix MySQLdb compatibiity issues +- Fix wrong default key value for Builder.lists() method + +### 0.3 + +(April 3th, 2015) + +- Query logging +- Polymorphic relations +- Adds support for Model.has() method +- Adds support for callbacks in eager load conditions +- Adds support for multi-threaded applications by default + + +### 0.2 + +(March 31th, 2015) + +- Adds actual ORM with relationships and eager loading +- Adds chunk support for QueryBuilder +- Properly close connections when using reconnect() and disconnect() methods + + +### 0.1 + +(March 18th, 2015) + +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..701df228 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015 Sébastien Eustace + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..bdafa7b6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.rst LICENSE +recursive-exclude tests * +recursive-exclude benchmark * diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a213ac02 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# This file is part of orator +# https://github.com/sdispater/orator + +# Licensed under the MIT license: +# http://www.opensource.org/licenses/MIT-license +# Copyright (c) 2015 Sébastien Eustace + +# lists all available targets +list: + @sh -c "$(MAKE) -p no_targets__ | \ + awk -F':' '/^[a-zA-Z0-9][^\$$#\/\\t=]*:([^=]|$$)/ {\ + split(\$$1,A,/ /);for(i in A)print A[i]\ + }' | grep -v '__\$$' | grep -v 'make\[1\]' | grep -v 'Makefile' | sort" +# required for list +no_targets__: + +# install all dependencies +setup: setup-python + +# test your application (tests in the tests/ directory) +test: + @py.test tests -sq + +# run tests against all supported python versions +tox: + @tox diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..305db31f --- /dev/null +++ b/README.rst @@ -0,0 +1,8 @@ +Pendulum +######## + +.. image:: https://travis-ci.org/sdispater/pendulum.png + :alt: Orator Build status + :target: https://travis-ci.org/sdispater/pendulum + +Dates and times for Python made easy. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..a3bcd22c --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,225 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pendulum.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pendulum.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Pendulum" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pendulum" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css new file mode 100644 index 00000000..b3370987 --- /dev/null +++ b/docs/_static/theme_overrides.css @@ -0,0 +1,1071 @@ +@import url(http://fonts.googleapis.com/css?family=Montserrat:400,700); +@import url(http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,500); + +body { + font: 400 13px/26px "Open Sans",sans-serif; + color: #687E95; + text-rendering: optimizelegibility; + background-color: #FFFFFF; +} + +.wy-body-for-nav { + background: #FFFFFF; +} + +.wy-nav-content { + background: #FFFFFF; +} + +.wy-nav-content-wrap { + background: #FFFFFF; + margin-left: 300px; + padding-left: 70px; + padding-right: 70px; + /*box-shadow: 0 0 1px rgba(0, 0, 0, 0.1);*/ + max-width: 940px; +} + +@media only screen and (max-width: 768px) { + .wy-nav-content-wrap { + margin-left: 0; + padding-left: 0; + padding-right: 0; + } +} + +.wy-nav-top { + background: #1A1A1A; +} + +.wy-plain-list-disc li, +.rst-content .section ul li, +.rst-content .toctree-wrapper ul li, +article ul li { + font: 400 13px/26px "Open Sans",sans-serif; + color: #687E95; +} + +.wy-nav-side { + position: fixed; + background: #1A1A1A; + /*box-shadow: 0px 0 1px rgba(0, 0, 0, 0.1) inset; + overflow: scroll; + bottom: inherit;*/ +} + +.wy-side-nav-search { + font-family: "Open Sans", "Roboto", sans-serif; + font-size: 15px; + background-color: transparent; + color: #7A7A7A; +} + +.wy-menu-vertical ul { + font-family: "Open Sans", "Roboto", sans-serif; + margin-right: 1em; +} + +.wy-menu-vertical > ul { + padding-left: 20px; +} + +.wy-menu-vertical li.current { + background: transparent; +} + +.wy-menu-vertical > ul > li { + margin-bottom: 30px; +} + +.wy-menu-vertical > ul > li > ul { + display: block; +} + +.wy-menu-vertical li.current > ul { + background: transparent; + padding: 0; +} + +.wy-menu-vertical li.on a, .wy-menu-vertical li.current > a { + background: transparent; + border: 0; + font-weight: 400; +} + +.wy-menu-vertical li a { + opacity: 1; +} + +.wy-menu-vertical > ul > li > a, +.wy-menu-vertical > ul > li > a:visited +{ + color: #9A9A9A; + opacity: 1; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -ms-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; +} + +.wy-menu-vertical > ul > li > a:hover +{ + color: #FAC322; + opacity: 1; +} + +.wy-menu-vertical li a:hover { + background: transparent; +} + +.wy-menu-vertical li a, .wy-menu-vertical li.current > a { + border-radius: 0 5px 5px 0; +} + +.wy-menu-vertical li.current > ul > li > a { + font-size: 13px; + border-right: 0; + opacity: 1; + padding: 0.4045em 20px; +} + +.wy-menu-vertical li.current > a { + font-size: 13px; + border-right: 0; +} + +.wy-menu-vertical li.toctree-l2.current > a { + background: none; + font-size: 13px; + color: #EF6155 !important; + padding-left: 3.027em; +} + +.wy-menu-vertical li.toctree-l1.current > ul > li > a::before { + font-family: "FontAwesome"; + content: ""; + color: #EC7600; + font-size: 8px; + position: absolute; + top: 4px; + left: 3em; + opacity: 0.3; + display: none; +} + +.wy-menu-vertical li.toctree-l1.current > ul > li.toctree-l2.current > a::before { + content: "##"; + color: #EF6155; + font-size: 13px; + position: absolute; + top: 6px; + left: 20px; + opacity: 1; + display: block; +} + +.wy-menu-vertical li.toctree-l2.current ul { + display: none !important; +} + +.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { + background: none; + padding: 0 4em; + font-size: 12px; +} + +.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current ul { + display: None; +} + +.wy-menu-vertical a, +.wy-menu-vertical a:visited { + color: #7A7A7A; + line-height: 20px; + font-size: 13px; +} + +.wy-menu-vertical a:hover { + color: #5A5A5A; +} + +.wy-menu-vertical > ul > li > ul > li > a, +.wy-menu-vertical > ul > li > ul > li > a:visited, +.wy-menu-vertical li.current a, +.wy-menu-vertical li.current a:visited { + color: #7A7A7A; + background: transparent; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -ms-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; +} + +.wy-menu-vertical > ul > li > ul > li > a:hover, +.wy-menu-vertical li.current a:hover { + color: #EF6155; + background: transparent; +} + +.wy-menu-vertical li.current > a { + color: #579ED1 !important; + padding-left: 2.427em; +} + +.wy-menu-vertical li.current > a:hover { +} + +.wy-side-nav-search input[type="text"] { + font-family: "Open Sans", "Roboto", sans-serif; + border: 0; + background: none; + box-shadow: none; + color: #7A7A7A; + font-size: 13px; + width: 90%; + border-radius: 0; + border-bottom: 1px solid rgba(129, 150, 154, 0.7); + opacity: 1; + padding: 6px 0; +} + +.wy-side-nav-search > a, +.wy-side-nav-search > a:visited { + color: #9A9A9A; +} + +.wy-side-nav-search > a:hover { + background: none; +} + +.wy-menu-vertical li { + position: relative; +} + +.wy-menu-vertical li span.toctree-expand::before, +.wy-menu-vertical li.on a span.toctree-expand::before, +.wy-menu-vertical li.current > a span.toctree-expand::before, +.wy-menu-vertical li a span.toctree-expand, +.wy-menu-vertical li a span.toctree-expand:before { + display: none; +} + +.wy-menu-vertical li.current ul li a span.toctree-expand, +.wy-menu-vertical li.on a:hover span.toctree-expand, +.wy-menu-vertical li.current a:hover span.toctree-expand{ + display: none; +} + +.wy-menu-vertical li.toctree-l1.current:before { + content: "#"; + color: #579ED1; + font-size: 13px; + position: absolute; + top: 3px; + left: 20px; + z-index: 2; + display: block; +} + +.wy-menu-vertical li.toctree-l1:before { + font-family: FontAwesome; + content: ""; + opacity: 1; + color: #1999B3; + font-size: 10px; + top: 2px; + position: absolute; + left: 12px; + z-index: 2; + display: none; +} + +.rst-content table.docutils, +.rst-content table.field-list { + border: medium none; + width: 100%; +} + +.rst-content table.docutils thead th, +.rst-content table.field-list thead th +{ + font-family: "Montserrat", serif; + text-transform: uppercase; + color: #7A7A7A; + font-weight: 500; +} + +.rst-content table.docutils thead th, +.rst-content table.field-list thead th, +.rst-content table.docutils tbody td, +.rst-content table.field-list tbody td +{ + border-top: 0; + border-bottom: 1px solid rgba(230, 230, 230, 0.7); + border-left: 0; + border-right: 0; + padding: 20px; + white-space: pre-wrap; +} + +.wy-table-odd td, +.wy-table-striped tr:nth-child(2n-1) td, +.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { + background: none; +} + +.rst-content table.docutils tbody td tt.code, +.rst-content table.field-list tbody td tt.code, +.rst-content table.docutils tbody td code.code, +.rst-content table.field-list tbody td code.code +{ + border: 0; + background: none; + padding: 0.3em 0; + color: #26A6A6; +} + +.rst-content .highlighted { + border: 1px solid rgb(250, 195, 34); + padding: 0 4px; + border-radius: 3px; + color: #BA8300; + display: inline; + font-weight: inherit; + background: rgba(250, 195, 34, 0.6) none repeat scroll 0% 0%; +} + +h1, h2, h3, h4, h5, h6 { + color: #384E65; + padding: 10px 0; + font-family: "Open Sans", "Montserrat", serif; + line-height: 1; + margin: 40px 0 30px 0; + font-weight: 300; + position: relative; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +.rst-content h1 .headerlink, +.rst-content h2 .headerlink, +.rst-content h3 .headerlink, +.rst-content h4 .headerlink, +.rst-content h5 .headerlink, +.rst-content h6 .headerlink { + visibility: hidden; + color: inherit; + font-size: inherit; + margin: 0; +} + +.rst-content h1 .headerlink:before, +.rst-content h2 .headerlink:before, +.rst-content h3 .headerlink:before, +.rst-content h4 .headerlink:before, +.rst-content h5 .headerlink:before, +.rst-content h6 .headerlink:before { + font-family: inherit; + visibility: visible; + position: absolute; + left: -40px; + width: 100%; + content: "#"; + opacity: 0; + font-weight: inherit; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -ms-transition: all 0.3s; + -o-transition: all 0.3s; + transition: all 0.3s; +} + +.rst-content h1 .headerlink:before { + top: 20px; +} + +.rst-content h1:hover .headerlink, +.rst-content h2:hover .headerlink, +.rst-content h3:hover .headerlink, +.rst-content h4:hover .headerlink, +.rst-content h5:hover .headerlink, +.rst-content h6:hover .headerlink { + display: inline; + color: inherit; +} + +.rst-content h1:hover .headerlink:before, +.rst-content h2:hover .headerlink:before, +.rst-content h3:hover .headerlink:before, +.rst-content h4:hover .headerlink:before, +.rst-content h5:hover .headerlink:before, +.rst-content h6:hover .headerlink:before { + opacity: 1; +} + +.rst-content h1 .headerlink:after, +.rst-content h2 .headerlink:after, +.rst-content h3 .headerlink:after, +.rst-content h4 .headerlink:after, +.rst-content h5 .headerlink:after, +.rst-content h6 .headerlink:after { + visibility: hidden; + content: ""; +} + +h1 { + font-size: 44px; +} + +h2 { + font-size: 38px; + color: #579ED1; + position: relative; +} + +h2:before { + display: none; + font-family: "FontAwesome"; + content: "#"; + margin-left: -30px; + font-size: 38px; + top: 11px; + color: #579ED1 !important; + opacity: 0.6; + position: absolute; +} + +h3 { + font-size: 24px; +} + +h4 { + font-size: 20px; +} + + +h1, +h1 a, +.header h1, +.header h1 a { + font: 300 44px/66px "Open Sans", "Roboto","Montserrat",sans-serif; + margin-bottom: 30px; + color: #485E75; +} + +a, a:visited { + color: #009CD4; +} + +a:hover { + color: #48B0F7; +} + +a > em { + font-style: normal; +} + +p { + font: 400 13px/26px "Open Sans",sans-serif; + color: #687E95; +} + +b, strong { + font-weight: 600; +} + +pre, pre code, .rst-content tt, code.docutils, .rst-content dl:not(.docutils) code { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + color: #26A6A6; + background-color: transparent; + font-size: 13px; + font-weight: 400; + border: 0; + padding: 0; +} + +pre, code, .rst-content ttc { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + font-weight: 400; + border-radius: 3px; + font-size: 13px; + margin: 0px 2px; +} + +div[class^="highlight"] { + border: 0; + background: transparent; +} + +.higlight { + background: transparent; +} + +.highlight > pre, div[class^="highlight"] pre { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + border-left: 1px solid #F2F2F2; + font-size: 13px; + padding: 2% 4%; + margin-left: 1%; + color: #788EA5; + /*background-color: #FAFAFA;*/ +} + +code, .rst-content tt, .rst-content code { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; +} + +code { + color: #CB6077; + background-color: transparent; + font-size: 13px; + font-weight: 500; +} + +p > code, +.rst-content tt, +p > code.docutils { + color: #26A6A6; + padding: 0; + display: inline; + font-weight: 400; + border: 0; + background-color: transparent; +} + +.rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal, .rst-content dl:not(.docutils) code { + color: #1976BF; +} + +.rst-content .admonition-title { + display: none; +} + +.rst-content .note, +.rst-content .warning, +.rst-content .tip, +.rst-content .caution, +.rst-content .versionchanged, +.rst-content .versionadded +{ + padding: 4% 4%; + margin: 30px 0; + background: transparent; +} + +.rst-content .note .highlight > pre, +.rst-content .note div[class^="highlight"] pre, +.rst-content .warning .highlight > pre, +.rst-content .warning div[class^="highlight"] pre, +.rst-content .versionadded .highlight > pre, +.rst-content .versionadded div[class^="highlight"] pre, +.rst-content .tip .highlight > pre, +.rst-content .tip div[class^="highlight"] pre, +.rst-content .caution .highlight > pre, +.rst-content .caution div[class^="highlight"] pre { + margin-left: 0; + border-left: 0; +} + +.rst-content .note { + border-left: 1px dotted rgba(50, 154, 188, 0.3); + border-right: 1px dotted rgba(50, 154, 188, 0.3); + background: rgba(50, 154, 188, 0.02); + color: #3399BB !important; + box-shadow: 0 0 2px rgba(50, 154, 188, 0.1); +} + +.rst-content .tip { + border-left: 1px dotted rgba(73,182,127, 0.3); + border-right: 1px dotted rgba(73,182,127, 0.3); + background: rgba(73,182,127, 0.02); + color: #49B67F !important; + box-shadow: 0 0 2px rgba(73,182,127, 0.1); +} + +.rst-content .tip > p { + color: #49B67F !important; +} + +.rst-content .tip > p code { + color: #19864F !important; +} + +.rst-content .warning, +.rst-content .caution { + border-left: 1px dotted rgba(249, 145, 87, 0.3); + border-right: 1px dotted rgba(249, 145, 87, 0.3); + background: rgba(249, 145, 87, 0.02); + color: #F99157 !important; + box-shadow: 0 0 2px rgba(249, 145, 87, 0.1); +} + +.rst-content .note > p { + color: #39B !important; +} + +.rst-content .warning > p, +.rst-content .caution > p +{ + color: #F99157 !important; +} + +.rst-content .versionadded, +.rst-content .versionchanged +{ + border-left: 1px dotted rgba(102, 119, 187, 0.3); + border-right: 1px dotted rgba(102, 119, 187, 0.3); + background: rgba(102, 119, 187, 0.02); + color: #6677BB !important; + margin-bottom: 20px; + position: relative; + box-shadow: 0 0 2px rgba(102, 119, 187, 0.1); +} + +.rst-content .versionadded > p > span, +.rst-content .versionchanged > p > span +{ + font-family: "Montserrat", serif; + display: block; + font-size: 12px; + margin-bottom: 20px; + text-transform: uppercase; + color: #36479B !important; +} + +.rst-content .versionadded > p, +.rst-content .versionchanged > p +{ + color: #6677BB !important; +} + +.rst-content .versionadded > p code, +.rst-content .versionchanged > p code +{ + color: #36479B !important; +} + +.rst-content .versionadded > p, +.rst-content .versionchanged > p { + margin-bottom: 0; +} + +.rst-content .note .versionchanged +{ + padding-left: 4%; +} + +.note code { + border: 0; + padding: 0; + display: inline; +} + +.note ul li, +.warning ul li { + color: inherit !important; +} + +.warning code, +.warning tt { + border: 0; + padding: 0; + display: inline; + color: #D9664F !important; +} + +.btn-neutral, +.btn-neutral:visited { + color: #808080 !important; + font: 400 12px/20px Montserrat,Arial,sans-serif; + text-transform: uppercase; + padding: 6px 14px; + border-radius: 4px; + box-shadow: none; + border: 0; + transition: all 0.3s; + background-color: transparent !important; +} + +.btn-neutral:hover { + background-color: transparent !important; + color: rgba(25, 153, 179, 0.8) !important; + /*color: rgba(0, 168, 192, 1) !important;*/ +} + +.btn-neutral:focus { + background-color: transparent !important; + color: rgba(25, 153, 179, 1) !important; + box-shadow: none; + padding: 6px 14px; +} + +.btn-neutral span.fa { + transition: all 0.3s; +} + +.btn-neutral:hover span.fa-arrow-circle-left { + padding-right: 5px; +} + +.btn-neutral:hover span.fa-arrow-circle-right { + padding-left: 5px; +} + +.btn-neutral span.fa-arrow-circle-left:before { + content: '' +} + +.btn-neutral span.fa-arrow-circle-right:before { + content: '' +} + + +/* Autodoc */ +.rst-content table.field-list .field-name { + text-align: left; + white-space: nowrap; + padding-right: 10px; + width: 140px; + font-weight: 600; +} + +.rst-content table.field-list .field-body { + padding: 8px 16px; + text-align: left; + vertical-align: top; + border: 0; +} + +.rst-content table.field-list .field-body ul { + line-height: 0; +} + +.rst-content table.field-list .field-body ul li { + list-style: none; + margin-left: 0; + font-size: 100%; +} + +.rst-content table.field-list td p { + line-height: inherit; +} + +.rst-content table.field-list .field-body strong { + color: #F3888B; + font-weight: 400; + margin-top: 0; +} + +.rst-content dl:not(.docutils).class > dt { + border: 0; + background: transparent; + font-size: inherit; + margin-bottom: 24px; + color: #579ED1; + font-weight: 600; +} + +.rst-content dl:not(.docutils).class > dt > em, +.rst-content dl:not(.docutils).class > dt > em.property { + font-weight: 400; + font-style: normal; +} + +.rst-content dl:not(.docutils).class > dt > code { + color: #F9A167; + font-weight: 400; +} + +.rst-content dl:not(.docutils) dl.classmethod > dt, +.rst-content dl:not(.docutils) dl.method > dt { + border-left: 1px solid #F2F2F2; + background: transparent; + font-size: inherit; +} + +.rst-content dl:not(.docutils) dl.classmethod > dt > code, +.rst-content dl:not(.docutils) dl.method > dt > code { + color: #D9664F; + font-weight: 400; +} + +.rst-content dl:not(.docutils) dl.classmethod > dt > em, +.rst-content dl:not(.docutils) dl.method > dt > em { + font-weight: 400; +} + +.rst-content dl:not(.docutils) dl.classmethod > dt > em.property, +.rst-content dl:not(.docutils) dl.method > dt > em.property { + font-weight: 400; + color: #7AC; +} + + +/* Solarized Light + +For use with Jekyll and Pygments + +http://ethanschoonover.com/solarized + +SOLARIZED HEX ROLE +--------- -------- ------------------------------------------ +base01 #586e75 body text / default code / primary content +base1 #93a1a1 comments / secondary content +base3 #fdf6e3 background +orange #cb4b16 constants +red #dc322f regex, special keywords +blue #268bd2 reserved keywords +cyan #2aa198 strings, numbers +green #859900 operators, other keywords +*/ + +.highlight { background-color: #fdf6e3; color: #586e75 } +.highlight .c { color: #93a1a1 } /* Comment */ +.highlight .err { color: #586e75 } /* Error */ +.highlight .g { color: #586e75 } /* Generic */ +.highlight .k { color: #859900 } /* Keyword */ +.highlight .l { color: #586e75 } /* Literal */ +.highlight .n { color: #586e75 } /* Name */ +.highlight .o { color: #859900 } /* Operator */ +.highlight .x { color: #cb4b16 } /* Other */ +.highlight .p { color: #586e75 } /* Punctuation */ +.highlight .cm { color: #93a1a1 } /* Comment.Multiline */ +.highlight .cp { color: #859900 } /* Comment.Preproc */ +.highlight .c1 { color: #93a1a1 } /* Comment.Single */ +.highlight .cs { color: #859900 } /* Comment.Special */ +.highlight .gd { color: #2aa198 } /* Generic.Deleted */ +.highlight .ge { color: #586e75; font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #dc322f } /* Generic.Error */ +.highlight .gh { color: #cb4b16 } /* Generic.Heading */ +.highlight .gi { color: #859900 } /* Generic.Inserted */ +.highlight .go { color: #586e75 } /* Generic.Output */ +.highlight .gp { color: #586e75 } /* Generic.Prompt */ +.highlight .gs { color: #586e75; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #cb4b16 } /* Generic.Subheading */ +.highlight .gt { color: #586e75 } /* Generic.Traceback */ +.highlight .kc { color: #cb4b16 } /* Keyword.Constant */ +.highlight .kd { color: #268bd2 } /* Keyword.Declaration */ +.highlight .kn { color: #859900 } /* Keyword.Namespace */ +.highlight .kp { color: #859900 } /* Keyword.Pseudo */ +.highlight .kr { color: #268bd2 } /* Keyword.Reserved */ +.highlight .kt { color: #dc322f } /* Keyword.Type */ +.highlight .ld { color: #586e75 } /* Literal.Date */ +.highlight .m { color: #2aa198 } /* Literal.Number */ +.highlight .s { color: #2aa198 } /* Literal.String */ +.highlight .na { color: #586e75 } /* Name.Attribute */ +.highlight .nb { color: #B58900 } /* Name.Builtin */ +.highlight .nc { color: #268bd2 } /* Name.Class */ +.highlight .no { color: #cb4b16 } /* Name.Constant */ +/*.highlight .nd { color: #268bd2 }*/ /* Name.Decorator */ +.highlight .nd { color: #cb4b16 } +.highlight .ni { color: #cb4b16 } /* Name.Entity */ +.highlight .ne { color: #cb4b16 } /* Name.Exception */ +.highlight .nf { color: #268bd2 } /* Name.Function */ +.highlight .nl { color: #586e75 } /* Name.Label */ +.highlight .nn { color: #586e75 } /* Name.Namespace */ +.highlight .nx { color: #586e75 } /* Name.Other */ +.highlight .py { color: #586e75 } /* Name.Property */ +.highlight .nt { color: #268bd2 } /* Name.Tag */ +.highlight .nv { color: #268bd2 } /* Name.Variable */ +.highlight .ow { color: #859900 } /* Operator.Word */ +.highlight .w { color: #586e75 } /* Text.Whitespace */ +.highlight .mf { color: #ff9800 } /* Literal.Number.Float */ +.highlight .mh { color: #ff9800 } /* Literal.Number.Hex */ +.highlight .mi { color: #ff9800 } /* Literal.Number.Integer */ +.highlight .mo { color: #ff9800 } /* Literal.Number.Oct */ +.highlight .sb { color: #93a1a1 } /* Literal.String.Backtick */ +.highlight .sc { color: #2aa198 } /* Literal.String.Char */ +.highlight .sd { color: #586e75 } /* Literal.String.Doc */ +.highlight .s2 { color: #2aa198 } /* Literal.String.Double */ +.highlight .se { color: #cb4b16 } /* Literal.String.Escape */ +.highlight .sh { color: #586e75 } /* Literal.String.Heredoc */ +.highlight .si { color: #2aa198 } /* Literal.String.Interpol */ +.highlight .sx { color: #2aa198 } /* Literal.String.Other */ +.highlight .sr { color: #dc322f } /* Literal.String.Regex */ +.highlight .s1 { color: #2aa198 } /* Literal.String.Single */ +.highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ +.highlight .bp { color: #F2777A } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #F2777A } /* Name.Variable.Class */ +.highlight .vg { color: #F2777A } /* Name.Variable.Global */ +.highlight .vi { color: #F2777A } /* Name.Variable.Instance */ +.highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ + + +/***** Mocha *****/ +.highlight .s { color: #7BBDA4 } /* Literal.String */ +.highlight .s1 { color: #7BBDA4 } /* Literal.String */ +.highlight .si { color: #A89BB9 } /* Literal.String */ +.highlight .m { color: #F4BC87 } /* Literal.Number */ +.highlight .mf { color: #F4BC87 } /* Literal.Number */ +.highlight .mh { color: #F4BC87 } /* Literal.Number */ +.highlight .mi { color: #F4BC87 } /* Literal.Number */ +.highlight .mo { color: #F4BC87 } /* Literal.Number */ +.highlight .k { color: #BEB55B; font-weight: normal; } /* Keyword */ +.highlight .kn { color: #BEB55B; font-weight: normal; } /* Keyword */ +.highlight .ow { color: #BEB55B; font-weight: normal } /* Operator.Word */ +.highlight .nc { color: #D28B71; font-weight: 500 } /* Name.Class */ +.highlight .nf { color: #8AB3B5; font-weight: normal; } /* Name.Function */ +.highlight .bp { color: #F3888B } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #F3888B } /* Name.Variable.Class */ +.highlight .vg { color: #F3888B } /* Name.Variable.Global */ +.highlight .vi { color: #F3888B } /* Name.Variable.Instance */ +.highlight .nd { color: #BB9584 } /* Name.Decorator */ +.highlight .ne { color: #CB6077; font-weight: 400 } /* Name.Exception */ +.highlight .o { color: #8A8A8A } /* Operator */ +.highlight .n { color: #8A8A8A } +.highlight .c { color: #B8AFAD; font-style: normal; } +.highlight .sd { color: #B8AFAD; font-style: normal; } +.highlight .nl { color: #A89BB9 } /* Name.Label */ +.highlight .nn { color: #A89BB9 } /* Name.Namespace */ +.highlight .nx { color: #A89BB9 } /* Name.Other */ +.highlight .py { color: #A89BB9 } /* Name.Property */ + +/* Inline code */ + +tt.code, code.code { + border: 1px solid #F2F2F2; + font-weight: normal; + display: inline; + padding: 0.3em 0.5em; + background-color: #FAFAFA; +} +tt.code .name, code.code .name { color: #8A8A8A } +tt.code .operator, code.code .operator { color: #8A8A8A } +tt.code .punctuation, code.code .punctuation { color: #8A8A8A } +tt.code .string, code.code .string { color: #7BBDA4 } +tt.code .number, code.code .number { color: #F4BC87 } +tt.code .integer, code.code .integer { color: #F4BC87 } + + +/***** Eighties *****/ + +.highlight .s { color: #59B6CF } /* Literal.String */ +.highlight .s1 { color: #59B6CF } /* Literal.String */ +.highlight .si { color: #8986BF } /* Literal.String */ +.highlight .m { color: #F9768F } /* Literal.Number */ +.highlight .mf { color: #F9768F } /* Literal.Number */ +.highlight .mh { color: #F9768F } /* Literal.Number */ +.highlight .mi { color: #F9768F } /* Literal.Number */ +.highlight .mo { color: #F9768F } /* Literal.Number */ +.highlight .k { color: #579ED1; font-weight: normal; } /* Keyword */ +.highlight .kn { color: #579ED1; font-weight: normal; } /* Keyword */ +.highlight .ow { color: #579ED1; font-weight: normal } /* Operator.Word */ +.highlight .nc { color: #69B69F; font-weight: 400 } /* Name.Class */ +.highlight .nf { color: #8986BF; font-weight: normal; } /* Name.Function */ +.highlight .bp { color: #69B69F } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #69B69F } /* Name.Variable.Class */ +.highlight .vg { color: #69B69F } /* Name.Variable.Global */ +.highlight .vi { color: #69B69F } /* Name.Variable.Instance */ +.highlight .nd { color: #879ED1 } /* Name.Decorator */ +.highlight .ne { color: #CB6077; font-weight: 400 } /* Name.Exception */ +.highlight .o { color: #98AEC5 } /* Operator */ +.highlight .n { color: #788EA5 } +.highlight .c { color: #A8AFC2; font-style: normal; } +.highlight .sd { color: #A8AFC2; font-style: normal; } +.highlight .nl { color: #F9768F } /* Name.Label */ +.highlight .nn { color: #8986BF } /* Name.Namespace */ +.highlight .nx { color: #F9768F } /* Name.Other */ +.highlight .py { color: #F9768F } /* Name.Property */ +.highlight .nb { color: #63ABA3 } /* Name.Builtin */ +.highlight .p { color: #98AEC5 } /* Punctuation */ + +/* Inline code */ + +tt.code, code.code { + border: 1px solid #F2F2F2; + font-weight: normal; + display: inline; + padding: 0.3em 0.5em; + background-color: #FAFAFA; +} +tt.code .name, code.code .name { color: #788EA5 } +tt.code .operator, code.code .operator { color: #98AEC5 } +tt.code .punctuation, code.code .punctuation { color: #98AEC5 } +tt.code .string, code.code .string { color: #59B6CF } +tt.code .number, code.code .number { color: #F9768F } +tt.code .integer, code.code .integer { color: #F9768F } +tt.code .pseudo, code.code .pseudo { color: #69B69F } + +/* API */ +.rst-content dl:not(.docutils) .property { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + padding-right: 5px; +} +.rst-content dl:not(.docutils) em { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + font-style: normal; + color: #788EA5; +} + +.rst-content dl.class:not(.docutils) > dt > code { + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + color: #788EA5; +} + +.rst-content dl:not(.docutils) > dt > tt.descname, +.rst-content dl:not(.docutils) > dt > tt.descname, +.rst-content dl:not(.docutils) > dt > code.descname { + color: #69B69F; +} + +.rst-content dl.class:not(.docutils) > dt > .sig-paren, +.rst-content dl:not(.docutils) dl.classmethod > dt > .sig-paren, +.rst-content dl:not(.docutils) dl.method > dt > .sig-paren + +{ + font-family: "Source Code Pro", "Consolas", "Menlo", "Monaco", "Courier New", Courier, monospace; + color: #98AEC5; + font-weight: normal; + font-style: normal; +} + +.rst-content dl:not(.docutils) dl.classmethod > dt > code, +.rst-content dl:not(.docutils) dl.method > dt > code { + color: #8986BF; + font-weight: 400; +} + +.rst-content table.field-list .field-body strong { + color: #F9768F; + font-weight: 600; +} + +.o { + font-weight: normal; +} + +/* Dark Orator */ + +div[class^="highlight"] { + border: 0; + background: #152B39; + border-radius: 3px; +} + +div[class^="highlight"] > pre { + border: 0; + color: #98AEC5; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + padding: 4% 4%; +} + +.highlight .nn, .highlight .nc { color: #89C6BF; } +.highlight .c, .highlight .c1 { color: #727995; font-style: normal; } +.highlight .sd { color: #727995; font-style: normal; } +.highlight .p, .highlight .o, code.code .operator { color: #687E95 } +.highlight .n, code.code .name { color: #98AEC5 } +.highlight .l { color: #98AEC5; } + +.note .last pre, +.warning .last pre, +.tip .last pre, +.caution .last pre, +.versionchanged .last pre, +.versionadded .last pre { + padding-bottom: 4%; + margin-bottom: 0; +} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..44d47520 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Pendulum documentation build configuration file, created by +# sphinx-quickstart on Mon Jun 27 12:06:22 2016. +# +# 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('.')) + +# -- 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 = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Pendulum' +copyright = '2016, Sébastien Eustace' +author = 'Sébastien Eustace' + +# 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 = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = '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 themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = 'Pendulum v0.1' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Pendulumdoc' + +# -- 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, 'Pendulum.tex', 'Pendulum Documentation', + 'Sébastien Eustace', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pendulum', 'Pendulum Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Pendulum', 'Pendulum Documentation', + author, 'Pendulum', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..08b9141f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +.. Pendulum documentation master file, created by + sphinx-quickstart on Mon Jun 27 12:06:22 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Pendulum's documentation! +==================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/pendulum/__init__.py b/pendulum/__init__.py new file mode 100644 index 00000000..4e804790 --- /dev/null +++ b/pendulum/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .pendulum import Pendulum diff --git a/pendulum/_compat.py b/pendulum/_compat.py new file mode 100644 index 00000000..a9f4c58d --- /dev/null +++ b/pendulum/_compat.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +import sys + +PY2 = sys.version_info[0] == 2 +PY3K = sys.version_info[0] >= 3 +PY33 = sys.version_info >= (3, 3) diff --git a/pendulum/exceptions.py b/pendulum/exceptions.py new file mode 100644 index 00000000..2695c6db --- /dev/null +++ b/pendulum/exceptions.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + + +class PendulumException(BaseException): + + pass diff --git a/pendulum/pendulum.py b/pendulum/pendulum.py new file mode 100644 index 00000000..e31d777b --- /dev/null +++ b/pendulum/pendulum.py @@ -0,0 +1,1463 @@ +# -*- coding: utf-8 -*- + +from __future__ import division + +import re +import time as _time +import math +import calendar +import pytz +import tzlocal +import datetime +import dateparser + +from pytz.tzinfo import BaseTzInfo, tzinfo +from dateutil.relativedelta import relativedelta + +from .exceptions import PendulumException +from .utils import hybrid_property +from ._compat import PY33 + + +class Pendulum(object): + + # The day constants + SUNDAY = 0 + MONDAY = 1 + TUESDAY = 2 + WEDNESDAY = 3 + THURSDAY = 4 + FRIDAY = 5 + SATURDAY = 6 + + # Names of days of the week + _days = { + SUNDAY: 'Sunday', + MONDAY: 'Monday', + TUESDAY: 'Tuesday', + WEDNESDAY: 'Wednesday', + THURSDAY: 'Thursday', + FRIDAY: 'Friday', + SATURDAY: 'Saturday' + } + + # Number of X in Y. + YEARS_PER_CENTURY = 100 + YEARS_PER_DECADE = 10 + MONTHS_PER_YEAR = 12 + WEEKS_PER_YEAR = 52 + DAYS_PER_WEEK = 7 + HOURS_PER_DAY = 24 + MINUTES_PER_HOUR = 60 + SECONDS_PER_MINUTE = 60 + + # Formats + ATOM = '%Y-%m-%dT%H:%M:%S%P' + COOKIE = '%A, %d-%b-%Y %H:%M:%S %Z' + ISO8601 = '%Y-%m-%dT%H:%M:%S%P' + ISO8601_EXTENDED = '%Y-%m-%dT%H:%M:%S.%f%P' + RFC822 = '%a, %d %b %y %H:%M:%S %z' + RFC850 = '%A, %d-%b-%y %H:%M:%S %Z' + RFC1036 = '%a, %d %b %y %H:%M:%S %z' + RFC1123 = '%a, %d %b %Y %H:%M:%S %z' + RFC2822 = '%a, %d %b %Y %H:%M:%S %z' + RFC3339 = '%Y-%m-%dT%H:%M:%S%P' + RFC3339_EXTENDED = '%Y-%m-%dT%H:%M:%S.%f%P' + RSS = '%a, %d %b %Y %H:%M:%S %z' + W3C = '%Y-%m-%dT%H:%M:%S%P' + + # Default format to use for __str__ method when type juggling occurs. + DEFAULT_TO_STRING_FORMAT = ISO8601_EXTENDED + + _to_string_format = DEFAULT_TO_STRING_FORMAT + + # First day of week + _week_starts_at = MONDAY + + # Last day of week + _week_ends_at = SUNDAY + + # Weekend days + _weekend_days = [ + SATURDAY, + SUNDAY + ] + + _EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.UTC) + + @classmethod + def _safe_create_datetime_zone(cls, obj): + """ + Creates a timezone from a string, BaseTzInfo or integer offset. + + :param obj: str or tzinfo or int or None + + :rtype: BaseTzInfo + """ + if obj is None or obj == 'local': + return tzlocal.get_localzone() + + if isinstance(obj, tzinfo): + return obj + + if isinstance(obj, (int, float)): + timezone_offset = obj * 60 + + return pytz.FixedOffset(timezone_offset) + + tz = pytz.timezone(obj) + + return tz + + def __init__(self, year=None, month=None, day=None, + hour=None, minute=None, second=None, microsecond=None, + tzinfo=pytz.UTC): + """ + Constructor. + + :type time: str or None + + :type tz: BaseTzInfo or str or None + """ + self._tz = self._safe_create_datetime_zone(tzinfo) + + now = (datetime.datetime.utcnow() + .replace(tzinfo=pytz.UTC) + .astimezone(self._tz)) + if year is None: + year = now.year + + if month is None: + month = now.month + + if day is None: + day = now.day + + if hour is None: + hour = now.hour + minute = now.minute if minute is None else minute + second = now.second if second is None else second + microsecond = now.microsecond if microsecond is None else microsecond + else: + minute = 0 if minute is None else minute + second = 0 if second is None else second + microsecond = 0 if microsecond is None else microsecond + + self._year = year + self._month = month + self._day = day + self._hour = hour + self._minute = minute + self._second = second + self._microsecond = microsecond + + self._datetime = self._tz.localize(datetime.datetime( + year, month, day, + hour, minute, second, microsecond, + tzinfo=None + )) + + @classmethod + def instance(cls, dt): + """ + Create a Carbon instance from a datetime one. + + :param dt: A datetime instance + :type dt: datetime.datetime + + :rtype: Pendulum + """ + return cls( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, dt.microsecond, + tzinfo=dt.tzinfo + ) + + @classmethod + def parse(cls, time=None, tz=pytz.UTC): + """ + Create a Pendulum instance from a string. + + :param time: The time string + :type time: str + + :param tz: The timezone + :type tz: BaseTzInfo or str or None + + :rtype: Pendulum + """ + if time is None: + return cls(tzinfo=tz) + + if time == 'now': + return cls(tzinfo=None) + + dt = dateparser.parse(time, languages=['en']) + + if not dt: + raise PendulumException('Invalid time string "%s"' % time) + + return cls( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, dt.microsecond, + tzinfo=tz + ) + + @classmethod + def now(cls, tz=None): + """ + Get a Pendulum instance for the current date and time. + + :param tz: The timezone + :type tz: BaseTzInfo or str or None + + :rtype: Pendulum + """ + return cls(tzinfo=tz) + + @classmethod + def utcnow(cls): + """ + Get a Pendulum instance for the current date and time in UTC. + + :param tz: The timezone + :type tz: BaseTzInfo or str or None + + :rtype: Pendulum + """ + return cls.now(pytz.UTC) + + @classmethod + def today(cls, tz=None): + """ + Create a Pendulum instance for today. + + :param tz: The timezone + :type tz: BaseTzInfo or str or None + + :rtype: Pendulum + """ + return cls.now(tz).start_of_day() + + @classmethod + def tomorrow(cls, tz=None): + """ + Create a Pendulum instance for tomorrow. + + :param tz: The timezone + :type tz: BaseTzInfo or str or None + + :rtype: Pendulum + """ + return cls.today(tz).add_day() + + @classmethod + def yesterday(cls, tz=None): + """ + Create a Pendulum instance for yesterday. + + :param tz: The timezone + :type tz: BaseTzInfo or str or None + + :rtype: Pendulum + """ + return cls.today(tz).sub_day() + + # TODO: max_value + + # TODO: min_value + + @classmethod + def create_from_date(cls, year=None, month=None, day=None, tz=pytz.UTC): + """ + Create a Pendulum instance from just a date. + The time portion is set to now. + + :type year: int + :type month: int + :type day: int + :type tz: tzinfo or str or None + + :rtype: Pendulum + """ + return cls(year, month, day, tzinfo=tz) + + @classmethod + def create_from_time(cls, hour=None, minute=None, second=None, + microsecond=None, tz=pytz.UTC): + """ + Create a Pendulum instance from just a time. + The date portion is set to today. + + :type hour: int + :type minute: int + :type second: int + :type microsecond: int + :type tz: tzinfo or str or int or None + + :rtype: Pendulum + """ + return cls(hour=hour, minute=minute, second=second, + microsecond=microsecond, tzinfo=tz) + + @classmethod + def create_from_format(cls, time, fmt, tz=pytz.UTC): + """ + Create a Pendulum instance from a specific format. + + :param fmt: The format + :type fmt: str + + :param time: The time string + :type time: str + + :param tz: The timezone + :type tz: tzinfo or str or int or None + + :rtype: Pendulum + """ + dt = (datetime.datetime.strptime(time, fmt) + .replace(tzinfo=cls._safe_create_datetime_zone(tz))) + + return cls.instance(dt) + + @classmethod + def create_from_timestamp(cls, timestamp, tz=pytz.UTC): + """ + Create a Pendulum instance from a timestamp. + + :param timestamp: The timestamp + :type timestamp: int or float + + :param tz: The timezone + :type tz: tzinfo or str or int or None + + :rtype: Pendulum + """ + return cls.now(tz).set_timestamp(timestamp) + + @classmethod + def strptime(cls, time, fmt): + return cls.create_from_format(time, fmt) + + def copy(self): + """ + Get a copy of the instance. + + :rtype: Pendulum + """ + return self.instance(self._datetime) + + ### Getters/Setters + + @hybrid_property + def year(self): + return self._datetime.year + + @hybrid_property + def month(self): + return self._datetime.month + + @hybrid_property + def day(self): + return self._datetime.day + + @hybrid_property + def hour(self): + return self._datetime.hour + + @hybrid_property + def minute(self): + return self._datetime.minute + + @hybrid_property + def second(self): + return self._datetime.second + + @property + def day_of_week(self): + return int(self.format('%w')) + + @property + def day_of_year(self): + return int(self.format('%-j')) + + @property + def week_of_year(self): + return int(self.format('%-W')) + + @property + def days_in_month(self): + return calendar.monthrange(self.year, self.month)[1] + + @property + def timestamp(self): + return int(self.float_timestamp - self._datetime.microsecond / 1e6) + + @property + def float_timestamp(self): + # If Python > 3.3 we use the native function + # else we emulate it + if PY33: + return self._datetime.timestamp() + + if self._datetime.tzinfo is None: + return _time.mktime((self.year, self.month, self.day, + self.hour, self.minute, self.second, + -1, -1, -1)) + self.microsecond / 1e6 + + else: + return (self._datetime - self._EPOCH).total_seconds() + + @property + def week_of_month(self): + return math.ceil(self.day / self.DAYS_PER_WEEK) + + @property + def age(self): + return self.diff_in_years() + + @property + def quarter(self): + return int(math.ceil(self.month / 3)) + + @property + def offset(self): + return self.get_offset() + + @property + def offset_hours(self): + return int(self.get_offset() + / self.SECONDS_PER_MINUTE + / self.MINUTES_PER_HOUR) + + @property + def local(self): + return self.offset == self.copy().set_timezone(tzlocal.get_localzone()).offset + + @property + def utc(self): + return self.offset == 0 + + @property + def timezone(self): + return self.get_timezone() + + @property + def timezone_name(self): + return self.timezone.zone + + def get_timezone(self): + return self._tz + + def get_offset(self): + return int(self._datetime.utcoffset().total_seconds()) + + def set_date(self, year, month, day): + """ + Sets the current date to a different date. + + :param year: The year + :type year: int + + :param month: The month + :type month: int + + :param day: The day + :type day: int + + :rtype: Pendulum + """ + self._datetime = self._datetime.replace( + year=int(year), month=int(month), day=int(day) + ) + + return self + + def set_time(self, hour, minute, second): + """ + Sets the current time to a different time. + + :param hour: The hour + :type hour: int + + :param minute: The minute + :type minute: int + + :param second: The second + :type second: int + + :rtype: Pendulum + """ + self._datetime = self._datetime.replace( + hour=int(hour), minute=int(minute), second=int(second) + ) + + return self + + def set_date_time(self, year, month, day, hour, minute, second=0): + """ + Set the date and time all together. + + :type year: int + :type month: int + :type day: int + :type hour: int + :type minute: int + :type second: int + + :rtype: Pendulum + """ + return self.set_date(year, month, day).set_time(hour, minute, second) + + def set_time_from_string(self, time): + """ + Set the time by time string. + + :param time: The time string + :type time: str + + :rtype: Pendulum + """ + time = time.split(':') + + hour = time[0] + minute = time[1] if len(time) > 1 else 0 + second = time[2] if len(time) > 2 else 0 + + return self.set_time(hour, minute, second) + + def set_timezone(self, value): + """ + Set the instance's timezone from a string or object. + + :param value: The timezone + :type value: BaseTzInfo or str or None + + :rtype: Pendulum + """ + self._tz = self._safe_create_datetime_zone(value) + self._datetime = self._datetime.astimezone(self._tz) + + return self + + def to(self, tz): + """ + Set the instance's timezone from a string or object. + + :param value: The timezone + :type value: BaseTzInfo or str or None + + :rtype: Pendulum + """ + return self.set_timezone(tz) + + def set_timestamp(self, timestamp): + """ + Set the date and time based on a Unix timestamp. + + :param timestamp: The timestamp + :type timestamp: int or float + + :rtype: Pendulum + """ + self._datetime = datetime.datetime.fromtimestamp(timestamp, pytz.UTC).astimezone(self._tz) + + return self + + ### Special week days + + @classmethod + def get_week_starts_at(cls): + """ + Get the first day of the week. + + :rtype: int + """ + return cls._week_starts_at + + @classmethod + def set_week_starts_at(cls, value): + """ + Set the first day of the week. + + :type value: int + """ + cls._week_starts_at = value + + @classmethod + def get_week_ends_at(cls): + """ + Get the last day of the week. + + :rtype: int + """ + return cls._week_ends_at + + @classmethod + def set_week_ends_at(cls, value): + """ + Set the last day of the week. + + :type value: int + """ + cls._week_ends_at = value + + @classmethod + def get_weekend_days(cls): + """ + Get weekend days. + + :rtype: list + """ + return cls._weekend_days + + @classmethod + def set_weekend_days(cls, value): + """ + Set weekend days. + + :type value: list + """ + cls._weekend_days = value + + # TODO: Testing aids + + # TODO: Localization + + @classmethod + def reset_to_string_format(cls): + """ + Reset the format used to the default + when type juggling a Pendulum instance to a string. + """ + cls.set_to_string_format(cls.DEFAULT_TO_STRING_FORMAT) + + @classmethod + def set_to_string_format(cls, fmt): + """ + Set the default format used + when type juggling a Pendulum instance to a string + + :type fmt: str + """ + cls._to_string_format = fmt + + def format(self, fmt): + """ + Formats the Pendulum instance using the given format. + + :param fmt: The format to use + :type fmt: str + + :rtype: str + """ + return self.strftime(fmt) + + def strftime(self, fmt): + """ + Formats the Pendulum instance using the given format. + + :param fmt: The format to use + :type fmt: str + + :rtype: str + """ + if not fmt: + return '' + + # Checking for custom formatters + custom_formatters = ['P'] + formatters_regex = re.compile('(.*)(?:%%(%s))(.*)' % '|'.join(custom_formatters)) + + m = re.match(formatters_regex, fmt) + if m and m.group(2): + return self.strftime(m.group(1)) + self._strftime(m.group(2)) + self.strftime(m.group(3)) + + return self._datetime.strftime(fmt) + + def _strftime(self, fmt): + """ + Handles custom formatters in format string. + + :return: str + """ + if fmt == 'P': + offset = self._datetime.utcoffset() or datetime.timedelta() + minutes = offset.total_seconds() / 60 + + if minutes > 0: + sign = '+' + else: + sign = '-' + + hour, minute = divmod(abs(int(minutes)), 60) + + return '{0}{1:02d}:{2:02d}'.format(sign, hour, minute) + + raise PendulumException('Unknown formatter %%%s' % fmt) + + def __str__(self): + return self.format(self._to_string_format) + + def to_date_string(self): + """ + Format the instance as date. + + :rtype: str + """ + return self.format('%Y-%m-%d') + + def to_formatted_date_string(self): + """ + Format the instance as a readable date. + + :rtype: str + """ + return self.format('%b %d, %Y') + + def to_time_string(self): + """ + Format the instance as time. + + :rtype: str + """ + return self.format('%H:%M:%S') + + def to_datetime_string(self): + """ + Format the instance as date and time. + + :rtype: str + """ + return self.format('%Y-%m-%d %H:%M:%S') + + def to_day_datetime_string(self): + """ + Format the instance as day, date and time. + + :rtype: str + """ + return self.format('%a, %b %d, %Y %-I:%M %p') + + def to_atom_string(self): + """ + Format the instance as ATOM. + + :rtype: str + """ + return self.format(self.ATOM) + + def to_cookie_string(self): + """ + Format the instance as COOKIE. + + :rtype: str + """ + return self.format(self.COOKIE) + + def to_iso8601_string(self, extended=False): + """ + Format the instance as ISO8601. + + :rtype: str + """ + fmt = self.ISO8601 + if extended: + fmt = self.ISO8601_EXTENDED + + return self.format(fmt) + + def to_rfc822_string(self): + """ + Format the instance as RFC822. + + :rtype: str + """ + return self.format(self.RFC822) + + def to_rfc850_string(self): + """ + Format the instance as RFC850. + + :rtype: str + """ + return self.format(self.RFC850) + + def to_rfc1036_string(self): + """ + Format the instance as RFC1036. + + :rtype: str + """ + return self.format(self.RFC1036) + + def to_rfc1123_string(self): + """ + Format the instance as RFC1123. + + :rtype: str + """ + return self.format(self.RFC1123) + + def to_rfc2822_string(self): + """ + Format the instance as RFC2822. + + :rtype: str + """ + return self.format(self.RFC2822) + + def to_rfc3339_string(self, extended=False): + """ + Format the instance as RFC3339. + + :rtype: str + """ + fmt = self.RFC3339 + if extended: + fmt = self.RFC3339_EXTENDED + + return self.format(fmt) + + def to_rss_string(self): + """ + Format the instance as RSS. + + :rtype: str + """ + return self.format(self.RSS) + + def to_w3c_string(self): + """ + Format the instance as W3C. + + :rtype: str + """ + return self.format(self.W3C) + + # TODO: Comparisons + + def add_years(self, value): + """ + Add years to the instance. Positive $value travel forward while + negative $value travel into the past. + + :param value: The number of years + :type value: int + + :rtype: Pendulum + """ + return self.add(years=value) + + def add_year(self, value=1): + """ + Add a year to the instance. + + :param value: The number of years + :type value: int + + :rtype: Pendulum + """ + return self.add_years(value) + + def sub_years(self, value): + """ + Remove years from the instance. + + :param value: The number of years + :type value: int + + :rtype: Pendulum + """ + return self.sub(years=value) + + def sub_year(self, value=1): + """ + Remove a year from the instance. + + :param value: The number of years + :type value: int + + :rtype: Pendulum + """ + return self.sub_years(value) + + def add_months(self, value): + """ + Add months to the instance. Positive $value travel forward while + negative $value travel into the past. + + :param value: The number of months + :type value: int + + :rtype: Pendulum + """ + return self.add(months=value) + + def add_month(self, value=1): + """ + Add a month to the instance. + + :param value: The number of month + :type value: int + + :rtype: Pendulum + """ + return self.add_months(value) + + def sub_months(self, value): + """ + Remove months from the instance. + + :param value: The number of months + :type value: int + + :rtype: Pendulum + """ + return self.sub(months=value) + + def sub_month(self, value=1): + """ + Remove a month from the instance. + + :param value: The number of months + :type value: int + + :rtype: Pendulum + """ + return self.sub_months(value) + + # TODO: No overflow + + def add_days(self, value): + """ + Add days to the instance. Positive $value travel forward while + negative $value travel into the past. + + :param value: The number of days + :type value: int + + :rtype: Pendulum + """ + return self.add(days=value) + + def add_day(self, value=1): + """ + Add a day to the instance. + + :param value: The number of days + :type value: int + + :rtype: Pendulum + """ + return self.add_days(value) + + def sub_days(self, value): + """ + Remove days from the instance. + + :param value: The number of days + :type value: int + + :rtype: Pendulum + """ + return self.sub(days=value) + + def sub_day(self, value=1): + """ + Remove a day from the instance. + + :param value: The number of days + :type value: int + + :rtype: Pendulum + """ + return self.sub_days(value) + + def add_weekdays(self, value): + """ + Add weekdays to the instance. Positive $value travel forward while + negative $value travel into the past. + + :param value: The number of weekdays + :type value: int + + :rtype: Pendulum + """ + return self.add(weekdays=value) + + def add_weekday(self, value=1): + """ + Add a weekday to the instance. + + :param value: The number of weekdays + :type value: int + + :rtype: Pendulum + """ + return self.add_days(value) + + def sub_weekdays(self, value): + """ + Remove weekdays from the instance. + + :param value: The number of weekdays + :type value: int + + :rtype: Pendulum + """ + return self.sub(weekdays=value) + + def sub_weekday(self, value=1): + """ + Remove a weekday from the instance. + + :param value: The number of weekdays + :type value: int + + :rtype: Pendulum + """ + return self.sub_weekdays(value) + + # TODO: Remaining durations + + def add(self, years=0, months=0, weeks=0, days=0, + hours=0, minutes=0, seconds=0, microseconds=0, + weekdays=None): + """ + Add duration to the instance. + + :param years: The number of years + :type years: int + + :param months: The number of months + :type months: int + + :param weeks: The number of weeks + :type weeks: int + + :param days: The number of days + :type days: int + + :param hours: The number of hours + :type hours: int + + :param minutes: The number of minutes + :type minutes: int + + :param seconds: The number of seconds + :type seconds: int + + :param microseconds: The number of microseconds + :type microseconds: int + + :rtype: Pendulum + """ + delta = relativedelta( + years=years, months=months, weeks=weeks, days=days, + hours=hours, minutes=minutes, seconds=seconds, + microseconds=microseconds, weekday=weekdays + ) + self._datetime = self._datetime + delta + + return self + + def sub(self, years=0, months=0, weeks=0, days=0, + hours=0, minutes=0, seconds=0, microseconds=0, + weekdays=None): + """ + Remove duration from the instance. + + :param years: The number of years + :type years: int + + :param months: The number of months + :type months: int + + :param weeks: The number of weeks + :type weeks: int + + :param days: The number of days + :type days: int + + :param hours: The number of hours + :type hours: int + + :param minutes: The number of minutes + :type minutes: int + + :param seconds: The number of seconds + :type seconds: int + + :param microseconds: The number of microseconds + :type microseconds: int + + :rtype: Pendulum + """ + delta = relativedelta( + years=years, months=months, weeks=weeks, days=days, + hours=hours, minutes=minutes, seconds=seconds, + microseconds=microseconds, weekday=weekdays + ) + self._datetime = self._datetime - delta + + return self + + ### Modifiers + + def start_of_day(self): + """ + Reset the time to 00:00:00 + + :rtype: Pendulum + """ + return self.set_time(0, 0, 0) + + def end_of_day(self): + """ + Reset the time to 23:59:59 + + :rtype: Pendulum + """ + return self.set_time(23, 59, 59) + + def start_of_month(self): + """ + Reset the date to the first day of the month and the time to 00:00:00. + + :rtype: Pendulum + """ + return self.set_date_time(self.year, self.month, 1, 0, 0, 0) + + def end_of_month(self): + """ + Reset the date to the last day of the month and the time to 23:59:59. + + :rtype: Pendulum + """ + return self.set_date_time( + self.year, self.month, self.days_in_month, 23, 59, 59 + ) + + def start_of_year(self): + """ + Reset the date to the first day of the year and the time to 00:00:00. + + :rtype: Pendulum + """ + return self.set_date_time(self.year, 1, 1, 0, 0, 0) + + def end_of_year(self): + """ + Reset the date to the last day of the year and the time to 23:59:59. + + :rtype: Pendulum + """ + return self.set_date_time( + self.year, 12, 31, 23, 59, 59 + ) + + def start_of_decade(self): + """ + Reset the date to the first day of the decade + and the time to 00:00:00. + + :rtype: Pendulum + """ + year = self.year - self.year % self.YEARS_PER_DECADE + return self.set_date_time(year, 1, 1, 0, 0, 0) + + def end_of_decade(self): + """ + Reset the date to the last day of the decade + and the time to 23:59:59. + + :rtype: Pendulum + """ + year = self.year - self.year % self.YEARS_PER_DECADE + self.YEARS_PER_DECADE - 1 + + return self.set_date_time( + year, 12, 31, 23, 59, 59 + ) + + def start_of_century(self): + """ + Reset the date to the first day of the century + and the time to 00:00:00. + + :rtype: Pendulum + """ + year = self.year - 1 - (self.year - 1) % self.YEARS_PER_CENTURY + 1 + return self.set_date_time(year, 1, 1, 0, 0, 0) + + def end_of_century(self): + """ + Reset the date to the last day of the century + and the time to 23:59:59. + + :rtype: Pendulum + """ + year = self.year - 1 - (self.year - 1) % self.YEARS_PER_CENTURY + self.YEARS_PER_CENTURY + + return self.set_date_time( + year, 12, 31, 23, 59, 59 + ) + + def start_of_week(self): + """ + Reset the date to the first day of the week + and the time to 00:00:00. + + :rtype: Pendulum + """ + if self.day_of_week != self._week_starts_at: + self.previous(self._week_starts_at) + + return self.start_of_day() + + def end_of_week(self): + """ + Reset the date to the last day of the week + and the time to 23:59:59. + + :rtype: Pendulum + """ + if self.day_of_week != self._week_ends_at: + self.next(self._week_ends_at) + + return self.end_of_day() + + def next(self, day_of_week=None): + """ + Modify to the next occurrence of a given day of the week. + If no day_of_week is provided, modify to the next occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :param day_of_week: The next day of week to reset to. + :type day_of_week: int or None + + :rtype: Pendulum + """ + if day_of_week is None: + day_of_week = self.day_of_week + + dt = self.start_of_day().add_day() + while dt.day_of_week != day_of_week: + dt.add_day() + + return dt + + def previous(self, day_of_week=None): + """ + Modify to the previous occurrence of a given day of the week. + If no day_of_week is provided, modify to the previous occurrence + of the current day of the week. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :param day_of_week: The previous day of week to reset to. + :type day_of_week: int or None + + :rtype: Pendulum + """ + if day_of_week is None: + day_of_week = self.day_of_week + + dt = self.start_of_day().sub_day() + while dt.day_of_week != day_of_week: + dt.sub_day() + + return dt + + def first_of_month(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the first day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Pendulum + """ + self.start_of_day() + + if day_of_week is None: + return self.day(1) + + month = calendar.monthcalendar(self.year, self.month) + + calendar_day = (day_of_week - 1) % 7 + + if month[0][calendar_day] > 0: + day_of_month = month[0][calendar_day] + else: + day_of_month = month[1][calendar_day] + + return self.day(day_of_month) + + def last_of_month(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current month. If no day_of_week is provided, + modify to the last day of the month. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Pendulum + """ + self.start_of_day() + + if day_of_week is None: + return self.day(self.days_in_month) + + month = calendar.monthcalendar(self.year, self.month) + + calendar_day = (day_of_week - 1) % 7 + + if month[-1][calendar_day] > 0: + day_of_month = month[-1][calendar_day] + else: + day_of_month = month[-2][calendar_day] + + return self.day(day_of_month) + + def nth_of_month(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current month. If the calculated occurrence is outside, + the scope of the current month, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: Pendulum + """ + if nth == 1: + return self.first_of_month(day_of_week) + + dt = self.copy().first_of_month() + check = dt.format('%Y-%m') + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt.next(day_of_week) + + if dt.format('%Y-%m') == check: + return self.day(dt.day).start_of_day() + + return False + + def first_of_quarter(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the first day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Pendulum + """ + return self.set_date(self.year, self.quarter * 3 - 2, 1).first_of_month(day_of_week) + + def last_of_quarter(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current quarter. If no day_of_week is provided, + modify to the last day of the quarter. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Pendulum + """ + return self.set_date(self.year, self.quarter * 3, 1).last_of_month(day_of_week) + + def nth_of_quarter(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current quarter. If the calculated occurrence is outside, + the scope of the current quarter, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: Pendulum + """ + if nth == 1: + return self.first_of_quarter(day_of_week) + + dt = self.copy().day(1).month(self.quarter * 3) + last_month = dt.month + year = dt.year + dt.first_of_quarter() + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt.next(day_of_week) + + if last_month < dt.month or year != dt.year: + return False + + return self.set_date(self.year, dt.month, dt.day).start_of_day() + + def first_of_year(self, day_of_week=None): + """ + Modify to the first occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the first day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Pendulum + """ + return self.month(1).first_of_month(day_of_week) + + def last_of_year(self, day_of_week=None): + """ + Modify to the last occurrence of a given day of the week + in the current year. If no day_of_week is provided, + modify to the last day of the year. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type day_of_week: int or None + + :rtype: Pendulum + """ + return self.month(self.MONTHS_PER_YEAR).last_of_month(day_of_week) + + def nth_of_year(self, nth, day_of_week): + """ + Modify to the given occurrence of a given day of the week + in the current year. If the calculated occurrence is outside, + the scope of the current year, then return False and no + modifications are made. Use the supplied consts + to indicate the desired day_of_week, ex. Pendulum.MONDAY. + + :type nth: int + + :type day_of_week: int or None + + :rtype: Pendulum + """ + if nth == 1: + return self.first_of_year(day_of_week) + + dt = self.copy().first_of_year() + year = dt.year + for i in range(nth - (1 if dt.day_of_week == day_of_week else 0)): + dt.next(day_of_week) + + if year != dt.year: + return False + + return self.set_date(self.year, dt.month, dt.day).start_of_day() + + def __getattr__(self, item): + return getattr(self._datetime, item) diff --git a/pendulum/utils.py b/pendulum/utils.py new file mode 100644 index 00000000..7364821d --- /dev/null +++ b/pendulum/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +from functools import update_wrapper +from wrapt import ObjectProxy + + +class PropertyProxy(ObjectProxy): + + _name = None + _instance = None + + def __init__(self, value, name, instance): + self._name = name + self._instance = instance + + super(PropertyProxy, self).__init__(value) + + def __call__(self, value): + self._instance._datetime = self._instance._datetime.replace(**{self._name: value}) + + return self._instance + + +class hybrid_property(object): + """ + Property that acts as a method to set value. + """ + + formats = { + 'year': '%Y', + #'year_iso': '%o', + 'month': '%-m', + 'day': '%-d', + 'hour': '%-H', + 'minute': '%-M', + 'second': '%-S' + } + + def __init__(self, func=None, name=None): + self._name = name + + self.set_func(func) + + def set_func(self, func): + self._func = func + + if self._name is None: + if isinstance(func, property): + self._name = func.fget.__name__ if func else None + else: + self._name = func.__name__ if func else None + + self.expr = func + if func is not None: + update_wrapper(self, func) + + def __get__(self, instance, owner): + if instance is None: + return self.expr + + return PropertyProxy(self._func(instance), self._name, instance) + + def __set__(self, instance, value): + if self._name == 'year': + instance.set_date(value, instance.month, instance.day) + elif self._name == 'month': + instance.set_date(instance.year, value, instance) + + def __call__(self, func): + self.set_func(func) + + return self diff --git a/pendulum/version.py b/pendulum/version.py new file mode 100644 index 00000000..e3a714f8 --- /dev/null +++ b/pendulum/version.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +VERSION = '0.1' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..2755223a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +tzlocal +pytz +dateparser +wrapt diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..44014a20 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import os +from setuptools import setup, find_packages + + +def get_version(): + basedir = os.path.dirname(__file__) + with open(os.path.join(basedir, 'pendulum/version.py')) as f: + variables = {} + exec(f.read(), variables) + + version = variables.get('VERSION') + if version: + return version + + raise RuntimeError('No version info found.') + + +__version__ = get_version() + +setup( + name='pendulum', + license='MIT', + version=__version__, + description='Dates and times in Python made easy.', + long_description=open('README.rst').read(), + author='Sébastien Eustace', + author_email='sebastien@eustace.io', + url='https://github.com/sdispater/pendulum', + download_url='https://github.com/sdispater/pendulum/archive/%s.tar.gz' % __version__, + packages=find_packages(exclude=['tests']), + install_requires=[ + 'tzlocal', + 'pytz', + 'dateparser', + 'wrapt' + ], + tests_require=['pytest'], + test_suite='nose.collector', + classifiers=[ + 'Intended Audience :: Developers', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) diff --git a/tests-requirements.txt b/tests-requirements.txt new file mode 100644 index 00000000..26b77f68 --- /dev/null +++ b/tests-requirements.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..a37eda39 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +import tzlocal +import pytz +from unittest import TestCase + +from pendulum import Pendulum + + +class AbstractTestCase(TestCase): + + def setUp(self): + self._save_tz = tzlocal.get_localzone + + tzlocal.get_localzone = lambda: pytz.timezone('America/Toronto') + + super(AbstractTestCase, self).setUp() + + def tearDown(self): + tzlocal.get_localzone = self._save_tz + Pendulum.reset_to_string_format() + + def assertPendulum(self, d, year, month, day, hour=None, minute=None, second=None): + self.assertEqual(year, d.year) + self.assertEqual(month, d.month) + self.assertEqual(day, d.day) + + if hour is not None: + self.assertEqual(hour, d.hour) + + if minute is not None: + self.assertEqual(minute, d.minute) + + if second is not None: + self.assertEqual(second, d.second) + + def assertIsInstanceOfPendulum(self, d): + self.assertIsInstance(d, Pendulum) diff --git a/tests/test_construct.py b/tests/test_construct.py new file mode 100644 index 00000000..3f24bb0d --- /dev/null +++ b/tests/test_construct.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +import pytz +from datetime import datetime +from pendulum import Pendulum +from . import AbstractTestCase + + +class ConstructTest(AbstractTestCase): + + def test_creates_an_instance_default_to_utcnow(self): + p = Pendulum() + now = Pendulum.utcnow() + self.assertIsInstanceOfPendulum(p) + self.assertEqual(p.timezone_name, now.timezone_name) + + self.assertPendulum(p, now.year, now.month, now.day, now.hour, now.minute, now.second) + + def test_parse_creates_an_instance_default_to_utcnow(self): + p = Pendulum.parse() + now = Pendulum.utcnow() + self.assertIsInstanceOfPendulum(p) + self.assertEqual(p.timezone_name, p.timezone_name) + + self.assertPendulum(p, now.year, now.month, now.day, now.hour, now.minute, now.second) + + def test_parse_with_fancy_string(self): + now = Pendulum() + p = Pendulum.parse('2 years and 2 months ago') + self.assertPendulum(p, now.year - 2, now.month - 2, now.day, now.hour, now.minute) + + def test_parse_with_default_timezone(self): + p = Pendulum.parse('now') + self.assertEqual('America/Toronto', p.timezone_name) + + def test_setting_timezone(self): + timezone = 'Europe/London' + dtz = pytz.timezone(timezone) + dt = datetime.utcnow() + offset = dtz.utcoffset(dt).total_seconds() / 3600 + + p = Pendulum(tzinfo=dtz) + self.assertEqual(timezone, p.timezone_name) + self.assertEqual(int(offset), p.offset_hours) + + def test_parse_setting_timezone(self): + timezone = 'Europe/London' + dtz = pytz.timezone(timezone) + dt = datetime.utcnow() + offset = dtz.utcoffset(dt).total_seconds() / 3600 + + p = Pendulum.parse(tz=dtz) + self.assertEqual(timezone, p.timezone_name) + self.assertEqual(int(offset), p.offset_hours) + + def test_setting_timezone_with_string(self): + timezone = 'Europe/London' + dtz = pytz.timezone(timezone) + dt = datetime.utcnow() + offset = dtz.utcoffset(dt).total_seconds() / 3600 + + p = Pendulum(tzinfo=timezone) + self.assertEqual(timezone, p.timezone_name) + self.assertEqual(int(offset), p.offset_hours) + + def test_parse_setting_timezone_with_string(self): + timezone = 'Europe/London' + dtz = pytz.timezone(timezone) + dt = datetime.utcnow() + offset = dtz.utcoffset(dt).total_seconds() / 3600 + + p = Pendulum.parse(tz=timezone) + self.assertEqual(timezone, p.timezone_name) + self.assertEqual(int(offset), p.offset_hours) + diff --git a/tests/test_create_from_date.py b/tests/test_create_from_date.py new file mode 100644 index 00000000..7c9a1583 --- /dev/null +++ b/tests/test_create_from_date.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +import pytz +from pendulum import Pendulum +from . import AbstractTestCase + + +class CreateFromDateTest(AbstractTestCase): + + def test_create_from_date_with_defaults(self): + d = Pendulum.create_from_date() + self.assertEqual(d.timestamp, Pendulum().timestamp) + + def test_create_from_date(self): + d = Pendulum.create_from_date(1975, 12, 25) + self.assertPendulum(d, 1975, 12, 25) + + def test_create_from_date_with_year(self): + d = Pendulum.create_from_date(1975) + self.assertEqual(d.year, 1975) + + def test_create_from_date_with_month(self): + d = Pendulum.create_from_date(None, 12) + self.assertEqual(d.month, 12) + d = Pendulum.create_from_date(month=12) + self.assertEqual(d.month, 12) + + def test_create_from_date_with_day(self): + d = Pendulum.create_from_date(None, None, 25) + self.assertEqual(d.day, 25) + d = Pendulum.create_from_date(day=25) + self.assertEqual(d.day, 25) + + def test_create_from_date_with_timezone(self): + d = Pendulum.create_from_date(1975, 12, 25, tz='Europe/London') + self.assertPendulum(d, 1975, 12, 25) + self.assertEqual('Europe/London', d.timezone_name) + + def test_create_from_date_with_tzinfo(self): + d = Pendulum.create_from_date(1975, 12, 25, tz=pytz.timezone('Europe/London')) + self.assertPendulum(d, 1975, 12, 25) + self.assertEqual('Europe/London', d.timezone_name) diff --git a/tests/test_create_from_format.py b/tests/test_create_from_format.py new file mode 100644 index 00000000..d6b3de12 --- /dev/null +++ b/tests/test_create_from_format.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +import pytz +from pendulum import Pendulum +from . import AbstractTestCase + + +class CreateFromFormatTest(AbstractTestCase): + + def test_create_from_format_returns_pendulum(self): + d = Pendulum.create_from_format('1975-05-21 22:32:11', '%Y-%m-%d %H:%M:%S') + self.assertPendulum(d, 1975, 5, 21, 22, 32, 11) + self.assertIsInstanceOfPendulum(d) + self.assertEqual('UTC', d.timezone_name) + + def test_create_from_format_with_timezone_string(self): + d = Pendulum.create_from_format('1975-05-21 22:32:11', '%Y-%m-%d %H:%M:%S', 'Europe/London') + self.assertPendulum(d, 1975, 5, 21, 22, 32, 11) + self.assertEqual('Europe/London', d.timezone_name) + + def test_create_from_format_with_timezone(self): + d = Pendulum.create_from_format( + '1975-05-21 22:32:11', '%Y-%m-%d %H:%M:%S', pytz.timezone('Europe/London') + ) + self.assertPendulum(d, 1975, 5, 21, 22, 32, 11) + self.assertEqual('Europe/London', d.timezone_name) + + def test_create_from_format_with_millis(self): + d = Pendulum.create_from_format( + '1975-05-21 22:32:11.123456', '%Y-%m-%d %H:%M:%S.%f' + ) + self.assertPendulum(d, 1975, 5, 21, 22, 32, 11) + self.assertEqual(123456, d.microsecond) diff --git a/tests/test_create_from_time.py b/tests/test_create_from_time.py new file mode 100644 index 00000000..1f7b55f8 --- /dev/null +++ b/tests/test_create_from_time.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +import pytz +from pendulum import Pendulum + +from . import AbstractTestCase + + +class CreateFromTimeTest(AbstractTestCase): + + def test_create_from_time_with_defaults(self): + d = Pendulum.create_from_time() + self.assertEqual(d.timestamp, Pendulum().timestamp) + self.assertEqual('UTC', d.timezone_name) + + def test_create_from_time(self): + d = Pendulum.create_from_time(23, 5, 11) + now = Pendulum.utcnow() + self.assertPendulum(d, now.year, now.month, now.day, 23, 5, 11) + self.assertEqual('UTC', d.timezone_name) + + def test_create_from_time_with_hour(self): + d = Pendulum.create_from_time(23) + self.assertEqual(23, d.hour) + self.assertEqual(0, d.minute) + self.assertEqual(0, d.second) + self.assertEqual(0, d.microsecond) + + def test_create_from_time_with_minute(self): + d = Pendulum.create_from_time(minute=5) + self.assertEqual(5, d.minute) + + def test_create_from_time_with_second(self): + d = Pendulum.create_from_time(second=11) + self.assertEqual(11, d.second) + + def test_create_from_time_with_timezone_string(self): + d = Pendulum.create_from_time(23, 5, 11, tz='Europe/London') + now = Pendulum.now('Europe/London') + self.assertPendulum(d, now.year, now.month, now.day, 23, 5, 11) + self.assertEqual('Europe/London', d.timezone_name) + + def test_create_from_time_with_timezone(self): + d = Pendulum.create_from_time(23, 5, 11, tz=pytz.timezone('Europe/London')) + now = Pendulum.now('Europe/London') + self.assertPendulum(d, now.year, now.month, now.day, 23, 5, 11) + self.assertEqual('Europe/London', d.timezone_name) diff --git a/tests/test_create_from_timestamp.py b/tests/test_create_from_timestamp.py new file mode 100644 index 00000000..9c12cfb4 --- /dev/null +++ b/tests/test_create_from_timestamp.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +import pytz +from pendulum import Pendulum + +from . import AbstractTestCase + + +class CreateFromTimestampTest(AbstractTestCase): + + def test_create_from_timestamp_returns_pendulum(self): + d = Pendulum.create_from_timestamp(Pendulum(1975, 5, 21, 22, 32, 5).timestamp) + self.assertPendulum(d, 1975, 5, 21, 22, 32, 5) + self.assertEqual('UTC', d.timezone_name) + + def test_create_from_timestamp_with_timezone_string(self): + d = Pendulum.create_from_timestamp(0, 'America/Toronto') + self.assertEqual('America/Toronto', d.timezone_name) + self.assertPendulum(d, 1969, 12, 31, 19, 0, 0) + + def test_create_from_timestamp_with_timezone(self): + d = Pendulum.create_from_timestamp(0, pytz.timezone('America/Toronto')) + self.assertEqual('America/Toronto', d.timezone_name) + self.assertPendulum(d, 1969, 12, 31, 19, 0, 0) diff --git a/tests/test_day_of_week_modifiers.py b/tests/test_day_of_week_modifiers.py new file mode 100644 index 00000000..661ef44a --- /dev/null +++ b/tests/test_day_of_week_modifiers.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- + +from pendulum import Pendulum + +from . import AbstractTestCase + + +class DayOfWeekModifiersTest(AbstractTestCase): + + def test_get_weekend_days(self): + self.assertEqual( + [Pendulum.SATURDAY, Pendulum.SUNDAY], + Pendulum.get_weekend_days() + ) + + def test_get_week_ends_at(self): + Pendulum.set_week_ends_at(Pendulum.SATURDAY) + self.assertEqual(Pendulum.get_week_ends_at(), Pendulum.SATURDAY) + Pendulum.set_week_ends_at(Pendulum.SUNDAY) + + def test_get_week_starts_at(self): + Pendulum.set_week_starts_at(Pendulum.TUESDAY) + self.assertEqual(Pendulum.get_week_starts_at(), Pendulum.TUESDAY) + Pendulum.set_week_starts_at(Pendulum.MONDAY) + + def test_start_of_week(self): + d = Pendulum(1980, 8, 7, 12, 11, 9).start_of_week() + self.assertPendulum(d, 1980, 8, 4, 0, 0, 0) + + def test_start_of_week_from_week_start(self): + d = Pendulum(1980, 8, 4).start_of_week() + self.assertPendulum(d, 1980, 8, 4, 0, 0, 0) + + def test_start_of_week_crossing_year_boundary(self): + d = Pendulum.create_from_date(2014, 1, 1).start_of_week() + self.assertPendulum(d, 2013, 12, 30, 0, 0, 0) + + def test_end_of_week(self): + d = Pendulum(1980, 8, 7, 12, 11, 9).end_of_week() + self.assertPendulum(d, 1980, 8, 10, 23, 59, 59) + + def test_end_of_week_from_week_end(self): + d = Pendulum(1980, 8, 10).end_of_week() + self.assertPendulum(d, 1980, 8, 10, 23, 59, 59) + + def test_end_of_week_crossing_year_boundary(self): + d = Pendulum.create_from_date(2013, 12, 31).end_of_week() + self.assertPendulum(d, 2014, 1, 5, 23, 59, 59) + + def test_next(self): + d = Pendulum.create_from_date(1975, 5, 21).next() + self.assertPendulum(d, 1975, 5, 28, 0, 0, 0) + + def test_next_monday(self): + d = Pendulum.create_from_date(1975, 5, 21).next(Pendulum.MONDAY) + self.assertPendulum(d, 1975, 5, 26, 0, 0, 0) + + def test_next_saturday(self): + d = Pendulum.create_from_date(1975, 5, 21).next(6) + self.assertPendulum(d, 1975, 5, 24, 0, 0, 0) + + def test_previous(self): + d = Pendulum.create_from_date(1975, 5, 21).previous() + self.assertPendulum(d, 1975, 5, 14, 0, 0, 0) + + def test_previous_monday(self): + d = Pendulum.create_from_date(1975, 5, 21).previous(Pendulum.MONDAY) + self.assertPendulum(d, 1975, 5, 19, 0, 0, 0) + + def test_previous_saturday(self): + d = Pendulum.create_from_date(1975, 5, 21).previous(6) + self.assertPendulum(d, 1975, 5, 17, 0, 0, 0) + + def test_first_day_of_month(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_month() + self.assertPendulum(d, 1975, 11, 1, 0, 0, 0) + + def test_first_wednesday_of_month(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_month(Pendulum.WEDNESDAY) + self.assertPendulum(d, 1975, 11, 5, 0, 0, 0) + + def test_first_friday_of_month(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_month(5) + self.assertPendulum(d, 1975, 11, 7, 0, 0, 0) + + def test_last_day_of_month(self): + d = Pendulum.create_from_date(1975, 12, 5).last_of_month() + self.assertPendulum(d, 1975, 12, 31, 0, 0, 0) + + def test_last_tuesday_of_month(self): + d = Pendulum.create_from_date(1975, 12, 1).last_of_month(Pendulum.TUESDAY) + self.assertPendulum(d, 1975, 12, 30, 0, 0, 0) + + def test_last_friday_of_month(self): + d = Pendulum.create_from_date(1975, 12, 5).last_of_month(5) + self.assertPendulum(d, 1975, 12, 26, 0, 0, 0) + + def test_nth_of_month_outside_scope(self): + d = Pendulum.create_from_date(1975, 12, 5) + + self.assertFalse(d.nth_of_month(6, Pendulum.MONDAY)) + + def test_nth_of_month_outside_year(self): + d = Pendulum.create_from_date(1975, 12, 5) + + self.assertFalse(d.nth_of_month(55, Pendulum.MONDAY)) + + def test_2nd_monday_of_month(self): + d = Pendulum.create_from_date(1975, 12, 5).nth_of_month(2, Pendulum.MONDAY) + + self.assertPendulum(d, 1975, 12, 8, 0, 0, 0) + + def test_3rd_wednesday_of_month(self): + d = Pendulum.create_from_date(1975, 12, 5).nth_of_month(3, 3) + + self.assertPendulum(d, 1975, 12, 17, 0, 0, 0) + + def test_first_day_of_quarter(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_quarter() + self.assertPendulum(d, 1975, 10, 1, 0, 0, 0) + + def test_first_wednesday_of_quarter(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_quarter(Pendulum.WEDNESDAY) + self.assertPendulum(d, 1975, 10, 1, 0, 0, 0) + + def test_first_friday_of_quarter(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_quarter(5) + self.assertPendulum(d, 1975, 10, 3, 0, 0, 0) + + def test_first_of_quarter_from_a_day_that_will_not_exist_in_the_first_month(self): + d = Pendulum.create_from_date(2014, 5, 31).first_of_quarter() + self.assertPendulum(d, 2014, 4, 1, 0, 0, 0) + + def test_last_day_of_quarter(self): + d = Pendulum.create_from_date(1975, 8, 5).last_of_quarter() + self.assertPendulum(d, 1975, 9, 30, 0, 0, 0) + + def test_last_tuesday_of_quarter(self): + d = Pendulum.create_from_date(1975, 8, 5).last_of_quarter(Pendulum.TUESDAY) + self.assertPendulum(d, 1975, 9, 30, 0, 0, 0) + + def test_last_friday_of_quarter(self): + d = Pendulum.create_from_date(1975, 8, 5).last_of_quarter(Pendulum.FRIDAY) + self.assertPendulum(d, 1975, 9, 26, 0, 0, 0) + + def test_last_day_of_quarter_that_will_not_exist_in_the_last_month(self): + d = Pendulum.create_from_date(2014, 5, 31).last_of_quarter() + self.assertPendulum(d, 2014, 6, 30, 0, 0, 0) + + def test_nth_of_quarter_outside_scope(self): + d = Pendulum.create_from_date(1975, 1, 5) + + self.assertFalse(d.nth_of_quarter(20, Pendulum.MONDAY)) + + def test_nth_of_quarter_outside_year(self): + d = Pendulum.create_from_date(1975, 1, 5) + + self.assertFalse(d.nth_of_quarter(55, Pendulum.MONDAY)) + + def test_nth_of_quarter_from_a_day_that_will_not_exist_in_the_first_month(self): + d = Pendulum.create_from_date(2014, 5, 31).nth_of_quarter(2, Pendulum.MONDAY) + self.assertPendulum(d, 2014, 4, 14, 0, 0, 0) + + def test_2nd_monday_of_quarter(self): + d = Pendulum.create_from_date(1975, 8, 5).nth_of_quarter(2, Pendulum.MONDAY) + self.assertPendulum(d, 1975, 7, 14, 0, 0, 0) + + def test_3rd_wednesday_of_quarter(self): + d = Pendulum.create_from_date(1975, 8, 5).nth_of_quarter(3, 3) + self.assertPendulum(d, 1975, 7, 16, 0, 0, 0) + + def test_first_day_of_year(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_year() + self.assertPendulum(d, 1975, 1, 1, 0, 0, 0) + + def test_first_wednesday_of_year(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_year(Pendulum.WEDNESDAY) + self.assertPendulum(d, 1975, 1, 1, 0, 0, 0) + + def test_first_friday_of_year(self): + d = Pendulum.create_from_date(1975, 11, 21).first_of_year(5) + self.assertPendulum(d, 1975, 1, 3, 0, 0, 0) + + def test_last_day_of_year(self): + d = Pendulum.create_from_date(1975, 8, 5).last_of_year() + self.assertPendulum(d, 1975, 12, 31, 0, 0, 0) + + def test_last_tuesday_of_year(self): + d = Pendulum.create_from_date(1975, 8, 5).last_of_year(Pendulum.TUESDAY) + self.assertPendulum(d, 1975, 12, 30, 0, 0, 0) + + def test_last_friday_of_year(self): + d = Pendulum.create_from_date(1975, 8, 5).last_of_year(5) + self.assertPendulum(d, 1975, 12, 26, 0, 0, 0) + + def test_nth_of_year_outside_scope(self): + d = Pendulum.create_from_date(1975, 1, 5) + + self.assertFalse(d.nth_of_year(55, Pendulum.MONDAY)) + + def test_2nd_monday_of_year(self): + d = Pendulum.create_from_date(1975, 8, 5).nth_of_year(2, Pendulum.MONDAY) + self.assertPendulum(d, 1975, 1, 13, 0, 0, 0) + + def test_2rd_wednesday_of_year(self): + d = Pendulum.create_from_date(1975, 8, 5).nth_of_year(3, Pendulum.WEDNESDAY) + self.assertPendulum(d, 1975, 1, 15, 0, 0, 0) + + def test_7th_thursday_of_year(self): + d = Pendulum.create_from_date(1975, 8, 31).nth_of_year(7, Pendulum.THURSDAY) + self.assertPendulum(d, 1975, 2, 13, 0, 0, 0) diff --git a/tests/test_strings.py b/tests/test_strings.py new file mode 100644 index 00000000..60109531 --- /dev/null +++ b/tests/test_strings.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +from pendulum import Pendulum +from . import AbstractTestCase + + +class StringsTest(AbstractTestCase): + + def test_to_string(self): + d = Pendulum(microsecond=0) + self.assertEqual(Pendulum(microsecond=0).to_iso8601_string(True), str(d)) + + def test_set_to_string_format(self): + Pendulum.set_to_string_format('%a, %d %b %y %H:%M:%S %z') + d = Pendulum(2016, 6, 27, 15, 39, 30) + self.assertEqual('Mon, 27 Jun 16 15:39:30 +0000', str(d)) + + def test_reset_to_string_format(self): + d = Pendulum(microsecond=0) + Pendulum.set_to_string_format('123') + Pendulum.reset_to_string_format() + self.assertEqual(d.to_iso8601_string(True), str(d)) + + def test_to_date_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16) + self.assertEqual('1975-12-25', d.to_date_string()) + + def test_to_formatted_date_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16) + self.assertEqual('Dec 25, 1975', d.to_formatted_date_string()) + + def test_to_localized_formatted_date_string(self): + # TODO + self.skipTest('Not yet implemented') + + def test_to_localized_formatted_timezoned_date_string(self): + # TODO + self.skipTest('Not yet implemented') + + def test_to_timestring(self): + d = Pendulum(1975, 12, 25, 14, 15, 16) + self.assertEqual('14:15:16', d.to_time_string()) + + def test_to_datetime_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16) + self.assertEqual('1975-12-25 14:15:16', d.to_datetime_string()) + + def test_to_datetime_string_with_padded_zeroes(self): + d = Pendulum(2000, 5, 2, 4, 3, 4) + self.assertEqual('2000-05-02 04:03:04', d.to_datetime_string()) + + def test_to_day_datetime_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16) + self.assertEqual('Thu, Dec 25, 1975 2:15 PM', d.to_day_datetime_string()) + + def test_to_atom_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('1975-12-25T14:15:16-05:00', d.to_atom_string()) + + def test_to_cookie_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thursday, 25-Dec-1975 14:15:16 EST', d.to_cookie_string()) + + def test_to_iso8601_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('1975-12-25T14:15:16-05:00', d.to_iso8601_string()) + + def test_to_iso8601_extended_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, 123456, tzinfo='local') + self.assertEqual('1975-12-25T14:15:16.123456-05:00', d.to_iso8601_string(True)) + + def test_to_rfc822_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thu, 25 Dec 75 14:15:16 -0500', d.to_rfc822_string()) + + def test_to_rfc850_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thursday, 25-Dec-75 14:15:16 EST', d.to_rfc850_string()) + + def test_to_rfc1036_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thu, 25 Dec 75 14:15:16 -0500', d.to_rfc1036_string()) + + def test_to_rfc1123_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thu, 25 Dec 1975 14:15:16 -0500', d.to_rfc1123_string()) + + def test_to_rfc2822_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thu, 25 Dec 1975 14:15:16 -0500', d.to_rfc2822_string()) + + def test_to_rfc3339_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('1975-12-25T14:15:16-05:00', d.to_rfc3339_string()) + + def test_to_rfc3339_extended_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, 123456, tzinfo='local') + self.assertEqual('1975-12-25T14:15:16.123456-05:00', d.to_rfc3339_string(True)) + + def test_to_rss_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('Thu, 25 Dec 1975 14:15:16 -0500', d.to_rss_string()) + + def test_to_w3c_string(self): + d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local') + self.assertEqual('1975-12-25T14:15:16-05:00', d.to_w3c_string()) diff --git a/tests/test_timezone.py b/tests/test_timezone.py new file mode 100644 index 00000000..78c3a4bf --- /dev/null +++ b/tests/test_timezone.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from pendulum import Pendulum + +from . import AbstractTestCase + + +class TimezoneTest(AbstractTestCase): + + def test_set_timezone(self): + d = Pendulum(2015, 1, 15, 18, 15, 34) + now = Pendulum(2015, 1, 15, 18, 15, 34) + self.assertEqual('UTC', d.timezone_name) + self.assertPendulum(d, now.year, now.month, now.day, now.hour, now.minute) + + d.set_timezone('Europe/Paris') + self.assertEqual('Europe/Paris', d.timezone_name) + self.assertPendulum(d, now.year, now.month, now.day, now.hour + 1, now.minute) + + def test_to(self): + d = Pendulum(2015, 1, 15, 18, 15, 34) + now = Pendulum(2015, 1, 15, 18, 15, 34) + self.assertEqual('UTC', d.timezone_name) + self.assertPendulum(d, now.year, now.month, now.day, now.hour, now.minute) + + d.to('Europe/Paris') + self.assertEqual('Europe/Paris', d.timezone_name) + self.assertPendulum(d, now.year, now.month, now.day, now.hour + 1, now.minute) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..c7c0ad35 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[tox] +envlist = py27, py35 + +[testenv] +deps = -rtests-requirements.txt +commands = py.test tests -sq + +[testenv:flake8] +basepython=python +deps=flake8 +commands= + flake8 pendulum