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