├── .bumpversion.cfg ├── .cookiecutterrc ├── .coveragerc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── ci ├── appveyor-bootstrap.py ├── appveyor-download.py ├── appveyor-with-compiler.cmd ├── bootstrap.py └── templates │ ├── .travis.yml │ └── appveyor.yml ├── docs ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── index.rst │ └── markov_novel.rst ├── requirements.txt ├── spelling_wordlist.txt └── usage.rst ├── setup.cfg ├── setup.py ├── src └── markov_novel │ ├── __init__.py │ ├── chapter.py │ ├── novel.py │ └── paragraph.py ├── tests ├── futuristmanifest.txt └── test_markov_novel.py └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:docs/conf.py] 9 | 10 | [bumpversion:file:src/markov_novel/__init__.py] 11 | 12 | -------------------------------------------------------------------------------- /.cookiecutterrc: -------------------------------------------------------------------------------- 1 | # This file exists so you can easily regenerate your project. 2 | # 3 | # `cookiepatcher` is a convenient shim around `cookiecutter` 4 | # for regenerating projects (it will generate a .cookiecutterrc 5 | # automatically for any template). To use it: 6 | # 7 | # pip install cookiepatcher 8 | # cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path 9 | # 10 | # See: 11 | # https://pypi.python.org/pypi/cookiecutter 12 | # 13 | # Alternatively, you can run: 14 | # 15 | # cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary 16 | 17 | default_context: 18 | 19 | appveyor: 'yes' 20 | c_extension_cython: 'no' 21 | c_extension_optional: 'no' 22 | c_extension_support: 'no' 23 | codacy: 'no' 24 | codeclimate: 'no' 25 | codecov: 'yes' 26 | command_line_interface: 'no' 27 | command_line_interface_bin_name: 'markov-novel' 28 | coveralls: 'yes' 29 | distribution_name: 'markov-novel' 30 | email: 'accraze@gmail.com' 31 | full_name: 'Andy Craze' 32 | github_username: 'accraze' 33 | landscape: 'no' 34 | package_name: 'markov_novel' 35 | project_name: 'markov-novel' 36 | project_short_description: 'Write a random novel using markov chains.' 37 | release_date: 'today' 38 | repo_name: 'python-markov-novel' 39 | requiresio: 'yes' 40 | scrutinizer: 'no' 41 | sphinx_doctest: 'no' 42 | sphinx_theme: 'sphinx-rtd-theme' 43 | test_matrix_configurator: 'no' 44 | test_matrix_separate_coverage: 'no' 45 | test_runner: 'pytest' 46 | travis: 'yes' 47 | version: '0.1.0' 48 | website: 'accraze.info' 49 | year: 'now' 50 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = 3 | src/markov_novel 4 | */site-packages/markov_novel 5 | 6 | [run] 7 | branch = true 8 | source = 9 | markov_novel 10 | tests 11 | parallel = true 12 | 13 | [report] 14 | show_missing = true 15 | precision = 2 16 | omit = *migrations* 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | 12 | [*.{bat,cmd,ps1}] 13 | end_of_line = crlf 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | wheelhouse 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | venv*/ 23 | pyvenv*/ 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | .coverage.* 32 | nosetests.xml 33 | coverage.xml 34 | htmlcov 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | .idea 44 | *.iml 45 | *.komodoproject 46 | 47 | # Complexity 48 | output/*.html 49 | output/*/index.html 50 | 51 | # Sphinx 52 | docs/_build 53 | 54 | .DS_Store 55 | *~ 56 | .*.sw[po] 57 | .build 58 | .ve 59 | .env 60 | .cache 61 | .pytest 62 | .bootstrap 63 | .appveyor.token 64 | *.bak 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | sudo: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | - TOXENV=check 10 | - TOXENV=docs 11 | 12 | - TOXENV=py27,coveralls,codecov 13 | - TOXENV=py33,coveralls,codecov 14 | - TOXENV=py34,coveralls,codecov 15 | - TOXENV=py35,coveralls,codecov 16 | - TOXENV=pypy,coveralls,codecov 17 | before_install: 18 | - python --version 19 | - uname -a 20 | - lsb_release -a 21 | install: 22 | - pip install tox 23 | - virtualenv --version 24 | - easy_install --version 25 | - pip --version 26 | - tox --version 27 | script: 28 | - tox -v 29 | after_failure: 30 | - more .tox/log/* | cat 31 | - more .tox/*/log/* | cat 32 | before_cache: 33 | - rm -rf $HOME/.cache/pip/log 34 | cache: 35 | directories: 36 | - $HOME/.cache/pip 37 | notifications: 38 | email: 39 | on_success: never 40 | on_failure: always 41 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Andy Craze - accraze.info 6 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | Changelog 3 | ========= 4 | 5 | 0.1.0 (2016-11-26) 6 | ----------------------------------------- 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | markov-novel could always use more documentation, whether as part of the 21 | official markov-novel docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests and feedback 25 | ============================= 26 | 27 | The best way to send feedback is to file an issue at https://github.com/accraze/python-markov-novel/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that code contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `python-markov-novel` for local development: 39 | 40 | 1. Fork `python-markov-novel `_ 41 | (look for the "Fork" button). 42 | 2. Clone your fork locally:: 43 | 44 | git clone git@github.com:your_name_here/python-markov-novel.git 45 | 46 | 3. Create a branch for local development:: 47 | 48 | git checkout -b name-of-your-bugfix-or-feature 49 | 50 | Now you can make your changes locally. 51 | 52 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 53 | 54 | tox 55 | 56 | 5. Commit your changes and push your branch to GitHub:: 57 | 58 | git add . 59 | git commit -m "Your detailed description of your changes." 60 | git push origin name-of-your-bugfix-or-feature 61 | 62 | 6. Submit a pull request through the GitHub website. 63 | 64 | Pull Request Guidelines 65 | ----------------------- 66 | 67 | If you need some code review or feedback while you're developing the code just make the pull request. 68 | 69 | For merging, you should: 70 | 71 | 1. Include passing tests (run ``tox``) [1]_. 72 | 2. Update documentation when there's new API, functionality etc. 73 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 74 | 4. Add yourself to ``AUTHORS.rst``. 75 | 76 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 77 | `run the tests `_ for each change you add in the pull request. 78 | 79 | It will be slower though ... 80 | 81 | Tips 82 | ---- 83 | 84 | To run a subset of tests:: 85 | 86 | tox -e envname -- py.test -k test_myfeature 87 | 88 | To run all the test environments in *parallel* (you need to ``pip install detox``):: 89 | 90 | detox 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Andy Craze 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 16 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 17 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 19 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | graft src 4 | graft ci 5 | graft tests 6 | 7 | include .bumpversion.cfg 8 | include .coveragerc 9 | include .cookiecutterrc 10 | include .editorconfig 11 | include .isort.cfg 12 | 13 | include AUTHORS.rst 14 | include CHANGELOG.rst 15 | include CONTRIBUTING.rst 16 | include LICENSE 17 | include README.rst 18 | 19 | include tox.ini .travis.yml appveyor.yml 20 | 21 | global-exclude *.py[cod] __pycache__ *.so *.dylib 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | .. start-badges 6 | 7 | .. list-table:: 8 | :stub-columns: 1 9 | 10 | * - docs 11 | - |docs| 12 | * - tests 13 | - | |travis| |appveyor| |requires| 14 | | |coveralls| |codecov| 15 | * - package 16 | - |version| |downloads| |wheel| |supported-versions| |supported-implementations| 17 | 18 | .. |docs| image:: https://readthedocs.org/projects/python-markov-novel/badge/?style=flat 19 | :target: https://readthedocs.org/projects/python-markov-novel 20 | :alt: Documentation Status 21 | 22 | .. |travis| image:: https://travis-ci.org/accraze/python-markov-novel.svg?branch=master 23 | :alt: Travis-CI Build Status 24 | :target: https://travis-ci.org/accraze/python-markov-novel 25 | 26 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/accraze/python-markov-novel?branch=master&svg=true 27 | :alt: AppVeyor Build Status 28 | :target: https://ci.appveyor.com/project/accraze/python-markov-novel 29 | 30 | .. |requires| image:: https://requires.io/github/accraze/python-markov-novel/requirements.svg?branch=master 31 | :alt: Requirements Status 32 | :target: https://requires.io/github/accraze/python-markov-novel/requirements/?branch=master 33 | 34 | .. |coveralls| image:: https://coveralls.io/repos/accraze/python-markov-novel/badge.svg?branch=master&service=github 35 | :alt: Coverage Status 36 | :target: https://coveralls.io/r/accraze/python-markov-novel 37 | 38 | .. |codecov| image:: https://codecov.io/github/accraze/python-markov-novel/coverage.svg?branch=master 39 | :alt: Coverage Status 40 | :target: https://codecov.io/github/accraze/python-markov-novel 41 | 42 | .. |version| image:: https://img.shields.io/pypi/v/markov-novel.svg?style=flat 43 | :alt: PyPI Package latest release 44 | :target: https://pypi.python.org/pypi/markov-novel 45 | 46 | .. |downloads| image:: https://img.shields.io/pypi/dm/markov-novel.svg?style=flat 47 | :alt: PyPI Package monthly downloads 48 | :target: https://pypi.python.org/pypi/markov-novel 49 | 50 | .. |wheel| image:: https://img.shields.io/pypi/wheel/markov-novel.svg?style=flat 51 | :alt: PyPI Wheel 52 | :target: https://pypi.python.org/pypi/markov-novel 53 | 54 | .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/markov-novel.svg?style=flat 55 | :alt: Supported versions 56 | :target: https://pypi.python.org/pypi/markov-novel 57 | 58 | .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/markov-novel.svg?style=flat 59 | :alt: Supported implementations 60 | :target: https://pypi.python.org/pypi/markov-novel 61 | 62 | 63 | .. end-badges 64 | 65 | Write a random novel using markov chains. 66 | 67 | * Free software: BSD license 68 | 69 | Installation 70 | ============ 71 | 72 | :: 73 | 74 | pip install markov-novel 75 | 76 | Quick Start 77 | =========== 78 | 79 | :: 80 | 81 | import markovify 82 | import markov_novel 83 | 84 | with open('path/to/corpus.txt') as f: 85 | text = f.read() 86 | # Build the model. 87 | text_model = markovify.Text(text) 88 | novel = markov_novel.Novel(text_model, chapter_count=1) 89 | novel.write(novel_title='my-novel', filetype='md') 90 | 91 | The novel will be written to your current working directory. 92 | 93 | Documentation 94 | ============= 95 | 96 | https://python-markov-novel.readthedocs.io/ 97 | 98 | Development 99 | =========== 100 | 101 | To run the all tests run:: 102 | 103 | tox 104 | 105 | Note, to combine the coverage data from all the tox environments run: 106 | 107 | .. list-table:: 108 | :widths: 10 90 109 | :stub-columns: 1 110 | 111 | - - Windows 112 | - :: 113 | 114 | set PYTEST_ADDOPTS=--cov-append 115 | tox 116 | 117 | - - Other 118 | - :: 119 | 120 | PYTEST_ADDOPTS=--cov-append tox 121 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | cache: 4 | - '%LOCALAPPDATA%\pip\Cache' 5 | environment: 6 | global: 7 | WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' 8 | matrix: 9 | - TOXENV: check 10 | PYTHON_HOME: C:\Python27 11 | PYTHON_VERSION: '2.7' 12 | PYTHON_ARCH: '32' 13 | 14 | - TOXENV: 'py27,codecov' 15 | TOXPYTHON: C:\Python27\python.exe 16 | PYTHON_HOME: C:\Python27 17 | PYTHON_VERSION: '2.7' 18 | PYTHON_ARCH: '32' 19 | 20 | - TOXENV: 'py27,codecov' 21 | TOXPYTHON: C:\Python27-x64\python.exe 22 | WINDOWS_SDK_VERSION: v7.0 23 | PYTHON_HOME: C:\Python27-x64 24 | PYTHON_VERSION: '2.7' 25 | PYTHON_ARCH: '64' 26 | 27 | - TOXENV: 'py34,codecov' 28 | TOXPYTHON: C:\Python34\python.exe 29 | PYTHON_HOME: C:\Python34 30 | PYTHON_VERSION: '3.4' 31 | PYTHON_ARCH: '32' 32 | 33 | - TOXENV: 'py34,codecov' 34 | TOXPYTHON: C:\Python34-x64\python.exe 35 | WINDOWS_SDK_VERSION: v7.1 36 | PYTHON_HOME: C:\Python34-x64 37 | PYTHON_VERSION: '3.4' 38 | PYTHON_ARCH: '64' 39 | 40 | - TOXENV: 'py35,codecov' 41 | TOXPYTHON: C:\Python35\python.exe 42 | PYTHON_HOME: C:\Python35 43 | PYTHON_VERSION: '3.5' 44 | PYTHON_ARCH: '32' 45 | 46 | - TOXENV: 'py35,codecov' 47 | TOXPYTHON: C:\Python35-x64\python.exe 48 | PYTHON_HOME: C:\Python35-x64 49 | PYTHON_VERSION: '3.5' 50 | PYTHON_ARCH: '64' 51 | 52 | init: 53 | - ps: echo $env:TOXENV 54 | - ps: ls C:\Python* 55 | install: 56 | - python -u ci\appveyor-bootstrap.py 57 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 58 | - '%PYTHON_HOME%\Scripts\easy_install --version' 59 | - '%PYTHON_HOME%\Scripts\pip --version' 60 | - '%PYTHON_HOME%\Scripts\tox --version' 61 | test_script: 62 | - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' 63 | 64 | on_failure: 65 | - ps: dir "env:" 66 | - ps: get-content .tox\*\log\* 67 | artifacts: 68 | - path: dist\* 69 | 70 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 71 | # on_finish: 72 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 73 | -------------------------------------------------------------------------------- /ci/appveyor-bootstrap.py: -------------------------------------------------------------------------------- 1 | """ 2 | AppVeyor will at least have few Pythons around so there's no point of implementing a bootstrapper in PowerShell. 3 | 4 | This is a port of https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 5 | with various fixes and improvements that just weren't feasible to implement in PowerShell. 6 | """ 7 | from __future__ import print_function 8 | 9 | from os import environ 10 | from os.path import exists 11 | from subprocess import CalledProcessError 12 | from subprocess import check_call 13 | 14 | try: 15 | from urllib.request import urlretrieve 16 | except ImportError: 17 | from urllib import urlretrieve 18 | 19 | BASE_URL = "https://www.python.org/ftp/python/" 20 | GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 21 | GET_PIP_PATH = "C:\get-pip.py" 22 | URLS = { 23 | ("2.7", "64"): BASE_URL + "2.7.10/python-2.7.10.amd64.msi", 24 | ("2.7", "32"): BASE_URL + "2.7.10/python-2.7.10.msi", 25 | # NOTE: no .msi installer for 3.3.6 26 | ("3.3", "64"): BASE_URL + "3.3.3/python-3.3.3.amd64.msi", 27 | ("3.3", "32"): BASE_URL + "3.3.3/python-3.3.3.msi", 28 | ("3.4", "64"): BASE_URL + "3.4.3/python-3.4.3.amd64.msi", 29 | ("3.4", "32"): BASE_URL + "3.4.3/python-3.4.3.msi", 30 | ("3.5", "64"): BASE_URL + "3.5.0/python-3.5.0-amd64.exe", 31 | ("3.5", "32"): BASE_URL + "3.5.0/python-3.5.0.exe", 32 | } 33 | INSTALL_CMD = { 34 | # Commands are allowed to fail only if they are not the last command. Eg: uninstall (/x) allowed to fail. 35 | "2.7": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 36 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 37 | "3.3": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 38 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 39 | "3.4": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], 40 | ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], 41 | "3.5": [["{path}", "/quiet", "TargetDir={home}"]], 42 | } 43 | 44 | 45 | def download_file(url, path): 46 | print("Downloading: {} (into {})".format(url, path)) 47 | progress = [0, 0] 48 | 49 | def report(count, size, total): 50 | progress[0] = count * size 51 | if progress[0] - progress[1] > 1000000: 52 | progress[1] = progress[0] 53 | print("Downloaded {:,}/{:,} ...".format(progress[1], total)) 54 | 55 | dest, _ = urlretrieve(url, path, reporthook=report) 56 | return dest 57 | 58 | 59 | def install_python(version, arch, home): 60 | print("Installing Python", version, "for", arch, "bit architecture to", home) 61 | if exists(home): 62 | return 63 | 64 | path = download_python(version, arch) 65 | print("Installing", path, "to", home) 66 | success = False 67 | for cmd in INSTALL_CMD[version]: 68 | cmd = [part.format(home=home, path=path) for part in cmd] 69 | print("Running:", " ".join(cmd)) 70 | try: 71 | check_call(cmd) 72 | except CalledProcessError as exc: 73 | print("Failed command", cmd, "with:", exc) 74 | if exists("install.log"): 75 | with open("install.log") as fh: 76 | print(fh.read()) 77 | else: 78 | success = True 79 | if success: 80 | print("Installation complete!") 81 | else: 82 | print("Installation failed") 83 | 84 | 85 | def download_python(version, arch): 86 | for _ in range(3): 87 | try: 88 | return download_file(URLS[version, arch], "installer.exe") 89 | except Exception as exc: 90 | print("Failed to download:", exc) 91 | print("Retrying ...") 92 | 93 | 94 | def install_pip(home): 95 | pip_path = home + "/Scripts/pip.exe" 96 | python_path = home + "/python.exe" 97 | if exists(pip_path): 98 | print("pip already installed.") 99 | else: 100 | print("Installing pip...") 101 | download_file(GET_PIP_URL, GET_PIP_PATH) 102 | print("Executing:", python_path, GET_PIP_PATH) 103 | check_call([python_path, GET_PIP_PATH]) 104 | 105 | 106 | def install_packages(home, *packages): 107 | cmd = [home + "/Scripts/pip.exe", "install"] 108 | cmd.extend(packages) 109 | check_call(cmd) 110 | 111 | 112 | if __name__ == "__main__": 113 | install_python(environ['PYTHON_VERSION'], environ['PYTHON_ARCH'], environ['PYTHON_HOME']) 114 | install_pip(environ['PYTHON_HOME']) 115 | install_packages(environ['PYTHON_HOME'], "setuptools>=18.0.1", "wheel", "tox", "virtualenv>=13.1.0") 116 | -------------------------------------------------------------------------------- /ci/appveyor-download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Use the AppVeyor API to download Windows artifacts. 4 | 5 | Taken from: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py 6 | # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 7 | # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | import argparse 12 | import os 13 | import zipfile 14 | 15 | import requests 16 | 17 | 18 | def make_auth_headers(): 19 | """Make the authentication headers needed to use the Appveyor API.""" 20 | path = os.path.expanduser("~/.appveyor.token") 21 | if not os.path.exists(path): 22 | raise RuntimeError( 23 | "Please create a file named `.appveyor.token` in your home directory. " 24 | "You can get the token from https://ci.appveyor.com/api-token" 25 | ) 26 | with open(path) as f: 27 | token = f.read().strip() 28 | 29 | headers = { 30 | 'Authorization': 'Bearer {}'.format(token), 31 | } 32 | return headers 33 | 34 | 35 | def download_latest_artifacts(account_project, build_id): 36 | """Download all the artifacts from the latest build.""" 37 | if build_id is None: 38 | url = "https://ci.appveyor.com/api/projects/{}".format(account_project) 39 | else: 40 | url = "https://ci.appveyor.com/api/projects/{}/build/{}".format(account_project, build_id) 41 | build = requests.get(url, headers=make_auth_headers()).json() 42 | jobs = build['build']['jobs'] 43 | print(u"Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) 44 | 45 | for job in jobs: 46 | name = job['name'] 47 | print(u" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) 48 | 49 | url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts".format(job['jobId']) 50 | response = requests.get(url, headers=make_auth_headers()) 51 | artifacts = response.json() 52 | 53 | for artifact in artifacts: 54 | is_zip = artifact['type'] == "Zip" 55 | filename = artifact['fileName'] 56 | print(u" {0}, {1} bytes".format(filename, artifact['size'])) 57 | 58 | url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts/{}".format(job['jobId'], filename) 59 | download_url(url, filename, make_auth_headers()) 60 | 61 | if is_zip: 62 | unpack_zipfile(filename) 63 | os.remove(filename) 64 | 65 | 66 | def ensure_dirs(filename): 67 | """Make sure the directories exist for `filename`.""" 68 | dirname = os.path.dirname(filename) 69 | if dirname and not os.path.exists(dirname): 70 | os.makedirs(dirname) 71 | 72 | 73 | def download_url(url, filename, headers): 74 | """Download a file from `url` to `filename`.""" 75 | ensure_dirs(filename) 76 | response = requests.get(url, headers=headers, stream=True) 77 | if response.status_code == 200: 78 | with open(filename, 'wb') as f: 79 | for chunk in response.iter_content(16 * 1024): 80 | f.write(chunk) 81 | else: 82 | print(u" Error downloading {}: {}".format(url, response)) 83 | 84 | 85 | def unpack_zipfile(filename): 86 | """Unpack a zipfile, using the names in the zip.""" 87 | with open(filename, 'rb') as fzip: 88 | z = zipfile.ZipFile(fzip) 89 | for name in z.namelist(): 90 | print(u" extracting {}".format(name)) 91 | ensure_dirs(name) 92 | z.extract(name) 93 | 94 | 95 | parser = argparse.ArgumentParser(description='Download artifacts from AppVeyor.') 96 | parser.add_argument('--id', 97 | metavar='PROJECT_ID', 98 | default='accraze/python-markov-novel', 99 | help='Project ID in AppVeyor.') 100 | parser.add_argument('build', 101 | nargs='?', 102 | metavar='BUILD_ID', 103 | help='Build ID in AppVeyor. Eg: master-123') 104 | 105 | if __name__ == "__main__": 106 | # import logging 107 | # logging.basicConfig(level="DEBUG") 108 | args = parser.parse_args() 109 | download_latest_artifacts(args.id, args.build) 110 | -------------------------------------------------------------------------------- /ci/appveyor-with-compiler.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds do not require specific environment configurations. 10 | :: 11 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 12 | :: cmd interpreter, at least for (SDK v7.0) 13 | :: 14 | :: More details at: 15 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 16 | :: http://stackoverflow.com/a/13751649/163740 17 | :: 18 | :: Author: Olivier Grisel 19 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 20 | SET COMMAND_TO_RUN=%* 21 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 22 | SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" 23 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 24 | 25 | 26 | IF "%PYTHON_VERSION%"=="3.5" ( 27 | IF EXIST %WIN_WDK% ( 28 | REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ 29 | REN %WIN_WDK% 0wdf 30 | ) 31 | GOTO main 32 | ) 33 | 34 | IF "%PYTHON_ARCH%"=="32" ( 35 | GOTO main 36 | ) 37 | 38 | SET DISTUTILS_USE_SDK=1 39 | SET MSSdk=1 40 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 41 | CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 42 | 43 | :main 44 | 45 | ECHO Executing: %COMMAND_TO_RUN% 46 | CALL %COMMAND_TO_RUN% || EXIT 1 47 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import sys 9 | from os.path import abspath 10 | from os.path import dirname 11 | from os.path import exists 12 | from os.path import join 13 | 14 | if __name__ == "__main__": 15 | base_path = dirname(dirname(abspath(__file__))) 16 | print("Project path: {0}".format(base_path)) 17 | env_path = join(base_path, ".tox", "bootstrap") 18 | if sys.platform == "win32": 19 | bin_path = join(env_path, "Scripts") 20 | else: 21 | bin_path = join(env_path, "bin") 22 | if not exists(env_path): 23 | import subprocess 24 | 25 | print("Making bootstrap env in: {0} ...".format(env_path)) 26 | try: 27 | subprocess.check_call(["virtualenv", env_path]) 28 | except subprocess.CalledProcessError: 29 | subprocess.check_call([sys.executable, "-m", "virtualenv", env_path]) 30 | print("Installing `jinja2` into bootstrap environment...") 31 | subprocess.check_call([join(bin_path, "pip"), "install", "jinja2"]) 32 | activate = join(bin_path, "activate_this.py") 33 | # noinspection PyCompatibility 34 | exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) 35 | 36 | import jinja2 37 | 38 | import subprocess 39 | 40 | jinja = jinja2.Environment( 41 | loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), 42 | trim_blocks=True, 43 | lstrip_blocks=True, 44 | keep_trailing_newline=True 45 | ) 46 | 47 | tox_environments = [ 48 | line.strip() 49 | # WARNING: 'tox' must be installed globally or in the project's virtualenv 50 | for line in subprocess.check_output(['tox', '--listenvs'], universal_newlines=True).splitlines() 51 | ] 52 | tox_environments = [line for line in tox_environments if line not in ['clean', 'report', 'docs', 'check']] 53 | 54 | for name in os.listdir(join("ci", "templates")): 55 | with open(join(base_path, name), "w") as fh: 56 | fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) 57 | print("Wrote {}".format(name)) 58 | print("DONE.") 59 | -------------------------------------------------------------------------------- /ci/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | sudo: false 4 | env: 5 | global: 6 | - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | - SEGFAULT_SIGNALS=all 8 | matrix: 9 | - TOXENV=check 10 | - TOXENV=docs 11 | {% for env in tox_environments %}{{ '' }} 12 | - TOXENV={{ env }},coveralls,codecov 13 | {% endfor %} 14 | 15 | before_install: 16 | - python --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | - virtualenv --version 22 | - easy_install --version 23 | - pip --version 24 | - tox --version 25 | script: 26 | - tox -v 27 | after_failure: 28 | - more .tox/log/* | cat 29 | - more .tox/*/log/* | cat 30 | before_cache: 31 | - rm -rf $HOME/.cache/pip/log 32 | cache: 33 | directories: 34 | - $HOME/.cache/pip 35 | notifications: 36 | email: 37 | on_success: never 38 | on_failure: always 39 | -------------------------------------------------------------------------------- /ci/templates/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | cache: 4 | - '%LOCALAPPDATA%\pip\Cache' 5 | environment: 6 | global: 7 | WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' 8 | matrix: 9 | - TOXENV: check 10 | PYTHON_HOME: C:\Python27 11 | PYTHON_VERSION: '2.7' 12 | PYTHON_ARCH: '32' 13 | 14 | {% for env in tox_environments %}{% if env.startswith(('py27', 'py34', 'py35')) %} 15 | - TOXENV: '{{ env }},codecov' 16 | TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe 17 | PYTHON_HOME: C:\Python{{ env[2:4] }} 18 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 19 | PYTHON_ARCH: '32' 20 | 21 | - TOXENV: '{{ env }},codecov' 22 | TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe 23 | {%- if env.startswith(('py2', 'py33', 'py34')) %} 24 | 25 | WINDOWS_SDK_VERSION: v7.{{ '1' if env.startswith('py3') else '0' }} 26 | {%- endif %} 27 | 28 | PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 29 | PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' 30 | PYTHON_ARCH: '64' 31 | 32 | {% endif %}{% endfor %} 33 | init: 34 | - ps: echo $env:TOXENV 35 | - ps: ls C:\Python* 36 | install: 37 | - python -u ci\appveyor-bootstrap.py 38 | - '%PYTHON_HOME%\Scripts\virtualenv --version' 39 | - '%PYTHON_HOME%\Scripts\easy_install --version' 40 | - '%PYTHON_HOME%\Scripts\pip --version' 41 | - '%PYTHON_HOME%\Scripts\tox --version' 42 | test_script: 43 | - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' 44 | 45 | on_failure: 46 | - ps: dir "env:" 47 | - ps: get-content .tox\*\log\* 48 | artifacts: 49 | - path: dist\* 50 | 51 | ### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): 52 | # on_finish: 53 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 54 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | extensions = [ 7 | 'sphinx.ext.autodoc', 8 | 'sphinx.ext.autosummary', 9 | 'sphinx.ext.coverage', 10 | 'sphinx.ext.doctest', 11 | 'sphinx.ext.extlinks', 12 | 'sphinx.ext.ifconfig', 13 | 'sphinx.ext.napoleon', 14 | 'sphinx.ext.todo', 15 | 'sphinx.ext.viewcode', 16 | ] 17 | if os.getenv('SPELLCHECK'): 18 | extensions += 'sphinxcontrib.spelling', 19 | spelling_show_suggestions = True 20 | spelling_lang = 'en_US' 21 | 22 | source_suffix = '.rst' 23 | master_doc = 'index' 24 | project = u'markov-novel' 25 | year = '2016' 26 | author = u'Andy Craze' 27 | copyright = '{0}, {1}'.format(year, author) 28 | version = release = u'0.1.0' 29 | 30 | pygments_style = 'trac' 31 | templates_path = ['.'] 32 | extlinks = { 33 | 'issue': ('https://github.com/accraze/python-markov-novel/issues/%s', '#'), 34 | 'pr': ('https://github.com/accraze/python-markov-novel/pull/%s', 'PR #'), 35 | } 36 | # on_rtd is whether we are on readthedocs.org 37 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 38 | 39 | if not on_rtd: # only set the theme if we're building docs locally 40 | html_theme = 'sphinx_rtd_theme' 41 | 42 | html_use_smartypants = True 43 | html_last_updated_fmt = '%b %d, %Y' 44 | html_split_index = False 45 | html_sidebars = { 46 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 47 | } 48 | html_short_title = '%s-%s' % (project, version) 49 | 50 | napoleon_use_ivar = True 51 | napoleon_use_rtype = False 52 | napoleon_use_param = False 53 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Contents 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | readme 9 | installation 10 | usage 11 | reference/index 12 | contributing 13 | authors 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install markov-novel 8 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | markov_novel* 8 | -------------------------------------------------------------------------------- /docs/reference/markov_novel.rst: -------------------------------------------------------------------------------- 1 | markov_novel 2 | ============ 3 | 4 | .. testsetup:: 5 | 6 | from markov_novel import * 7 | 8 | .. automodule:: markov_novel 9 | :members: 10 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.3 2 | sphinx-rtd-theme 3 | -e . 4 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use markov-novel in a project:: 6 | 7 | import markov_novel 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | max-line-length = 140 6 | exclude = tests/*,*/migrations/*,*/south_migrations/* 7 | 8 | [pytest] 9 | norecursedirs = 10 | .git 11 | .tox 12 | .env 13 | dist 14 | build 15 | south_migrations 16 | migrations 17 | python_files = 18 | test_*.py 19 | *_test.py 20 | tests.py 21 | addopts = 22 | -rxEfsw 23 | --strict 24 | --doctest-modules 25 | --doctest-glob=\*.rst 26 | --tb=short 27 | 28 | [isort] 29 | force_single_line=True 30 | line_length=120 31 | known_first_party=markov_novel 32 | default_section=THIRDPARTY 33 | forced_separate=test_markov_novel 34 | not_skip = __init__.py 35 | skip = migrations, south_migrations 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | 6 | import io 7 | import re 8 | from glob import glob 9 | from os.path import basename 10 | from os.path import dirname 11 | from os.path import join 12 | from os.path import splitext 13 | 14 | from setuptools import find_packages 15 | from setuptools import setup 16 | 17 | 18 | def read(*names, **kwargs): 19 | return io.open( 20 | join(dirname(__file__), *names), 21 | encoding=kwargs.get('encoding', 'utf8') 22 | ).read() 23 | 24 | 25 | setup( 26 | name='markov-novel', 27 | version='0.1.0', 28 | license='BSD', 29 | description='Write a random novel using markov chains.', 30 | long_description='%s\n%s' % ( 31 | re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), 32 | re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) 33 | ), 34 | author='Andy Craze', 35 | author_email='accraze@gmail.com', 36 | url='https://github.com/accraze/python-markov-novel', 37 | packages=find_packages('src'), 38 | package_dir={'': 'src'}, 39 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 40 | include_package_data=True, 41 | zip_safe=False, 42 | classifiers=[ 43 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 44 | 'Development Status :: 5 - Production/Stable', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: BSD License', 47 | 'Operating System :: Unix', 48 | 'Operating System :: POSIX', 49 | 'Operating System :: Microsoft :: Windows', 50 | 'Programming Language :: Python', 51 | 'Programming Language :: Python :: 2.7', 52 | 'Programming Language :: Python :: 3', 53 | 'Programming Language :: Python :: 3.3', 54 | 'Programming Language :: Python :: 3.4', 55 | 'Programming Language :: Python :: 3.5', 56 | 'Programming Language :: Python :: Implementation :: CPython', 57 | 'Programming Language :: Python :: Implementation :: PyPy', 58 | 'Topic :: Utilities', 59 | ], 60 | keywords=[ 61 | 'nlp', 'language', 'markov chain', 'novel', 'text', 'automation' 62 | ], 63 | install_requires=[ 64 | 'markovify==0.4.3' 65 | ], 66 | extras_require={ 67 | # eg: 68 | # 'rst': ['docutils>=0.11'], 69 | # ':python_version=="2.6"': ['argparse'], 70 | }, 71 | ) 72 | -------------------------------------------------------------------------------- /src/markov_novel/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | 3 | from .chapter import Chapter # noqa 4 | from .novel import Novel # noqa 5 | from .paragraph import Paragraph # noqa 6 | -------------------------------------------------------------------------------- /src/markov_novel/chapter.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | import markovify 4 | from .paragraph import Paragraph 5 | 6 | 7 | class Chapter(object): 8 | """ 9 | A chapter 10 | that contains paragraphs 11 | written by markov models. 12 | """ 13 | 14 | def __init__(self, model, title): 15 | if not isinstance(model, markovify.Text): 16 | raise Exception('model must be a markov model') 17 | self.model = model 18 | self.title = 'Chapter %d' % title 19 | 20 | def write_chapter(self): 21 | """ 22 | Create a chapter 23 | that contains a random number 24 | of paragraphs 25 | """ 26 | self.paragraphs = [] 27 | self.paragraphs.append('\n') 28 | for x in range(randint(0, 50)): 29 | p = Paragraph(self.model) 30 | self.paragraphs.append(p.get_paragraph()) 31 | self.paragraphs.append('\n') 32 | return self.paragraphs 33 | -------------------------------------------------------------------------------- /src/markov_novel/novel.py: -------------------------------------------------------------------------------- 1 | from .chapter import Chapter 2 | 3 | 4 | class Novel(object): 5 | """ 6 | Writes a bunch of 7 | Chapters to file 8 | """ 9 | 10 | def __init__(self, markov, chapter_count=1): 11 | self.chapters = [] 12 | self.markov = markov 13 | self.chapter_count = chapter_count 14 | 15 | def write(self, novel_title='novel', filetype='txt'): 16 | """ 17 | Composes chapters 18 | and writes the novel to a text file 19 | """ 20 | self._compose_chapters() 21 | self._write_to_file(novel_title, filetype) 22 | 23 | def _compose_chapters(self): 24 | """ 25 | Creates a chapters 26 | and appends them to list 27 | """ 28 | for count in range(self.chapter_count): 29 | chapter_num = count + 1 30 | c = Chapter(self.markov, chapter_num) 31 | self.chapters.append(c) 32 | 33 | def _write_to_file(self, novel_title, filetype): 34 | with open('%s.%s' % (novel_title, filetype), 'w') as f: 35 | for chapter in self.chapters: 36 | if filetype == 'md': 37 | f.write('## ' + chapter.title) 38 | else: 39 | f.write(chapter.title) 40 | f.write('\n') 41 | paragraphs = chapter.write_chapter() 42 | for paragraph in paragraphs: 43 | f.write(paragraph) 44 | f.write('\n') 45 | -------------------------------------------------------------------------------- /src/markov_novel/paragraph.py: -------------------------------------------------------------------------------- 1 | from random import randint 2 | 3 | import markovify 4 | 5 | 6 | class Paragraph(object): 7 | """ 8 | Object to hold 9 | multiple setences created by 10 | a Markov Model 11 | """ 12 | 13 | def __init__(self, model): 14 | if not isinstance(model, markovify.Text): 15 | raise Exception('model must be a markov model') 16 | self.model = model 17 | 18 | def get_paragraph(self): 19 | """ 20 | Write a paragraph 21 | of 5 sentences. 22 | """ 23 | self.text = '' 24 | for x in range(randint(5, 12)): 25 | sentence = self._write_sentence() 26 | self.text = self.text + sentence 27 | return self.text 28 | 29 | def _write_sentence(self): 30 | sentence = None 31 | while sentence is None: 32 | sentence = self.model.make_sentence() 33 | return sentence + ' ' 34 | -------------------------------------------------------------------------------- /tests/futuristmanifest.txt: -------------------------------------------------------------------------------- 1 | The Founding and Manifesto of 2 | Futurism 3 | 4 | 5 | Original publication in French: Le Figaro, Paris, February 20, 1909 6 | 7 | This English-language translation COPYRIGHT ©1973 Thames and 8 | Hudson Ltd, London. All rights reserved. 9 | 10 | 11 | Source for translation by R.W. Flint reproduced below: 12 | 13 | 14 | Apollonio, Umbro, ed. Documents of 20th Century Art: Futurist 15 | Manifestos. Brain, Robert, R.W. Flint, J.C. Higgitt, and Caroline Tisdall, 16 | trans. New York: Viking Press, 1973. 19-24. 17 | 18 | The Founding and 19 | Manifesto of Futurism 20 | by F. T. Marinetti 21 | 22 | We had stayed up all night, my friends and I, under hanging mosque lamps with domes of filigreed brass, 23 | domes starred like our spirits, shining like them with the prisoned radiance of electric hearts. For hours we had 24 | trampled our atavistic ennui into rich oriental rugs, arguing up to the last confines of logic and blackening many 25 | reams of paper with our frenzied scribbling. 26 | 27 | An immense pride was buoying us up, because we felt ourselves alone at that hour, alone, awake, and on our 28 | feet, like proud beacons or forward sentries against an army of hostile stars glaring down at us from their 29 | celestial encampments. Alone with stokers feeding the hellish fires of great ships, alone with the black spectres 30 | who grope in the red-hot bellies of locomotives launched on their crazy courses, alone with drunkards reeling 31 | like wounded birds along the city walls. 32 | 33 | Suddenly we jumped, hearing the mighty noise of the huge double-decker trams that rumbled by outside, 34 | ablaze with colored lights, like villages on holiday suddenly struck and uprooted by the flooding Po and dragged 35 | over falls and through gourges to the sea. 36 | 37 | Then the silence deepened. But, as we listened to the old canal muttering its feeble prayers and the creaking 38 | bones of sickly palaces above their damp green beards, under the windows we suddenly heard the famished 39 | roar of automobiles. 40 | 41 | 'Let's go!' I said. 'Friends, away! Let's go! Mythology and the Mystic Ideal are defeated at last. We're about to 42 | see the Centaur's birth and, soon after, the first flight of Angels! ... We must shake at the gates of life, test the 43 | bolts and hinges. Let's go! Look there, on the earth, the very first dawn! There's nothing to match the splendor 44 | of the sun's red sword, slashing for the first time through our millennial gloom!' 45 | 46 | We went up to the three snorting beasts, to lay amorous hands on their torrid breasts. I stretched out on my car 47 | like a corpse on its bier, but revived at once under the steering wheel, a guillotine blade that threatened my 48 | stomach. 49 | 50 | 51 | The raging broom of madness swept us out of ourselves and drove us through streets as rough and deep as the 52 | beds of torrents. Here and there, sick lamplight through window glass taught us to distrust the deceitful 53 | mathematics of our perishing eyes. 54 | 55 | I cried, 'The scent, the scent alone is enough for our beasts.' 56 | 57 | And like young lions we ran after Death, its dark pelt blotched with pale crosses as it escaped down the vast 58 | violet living and throbbing sky. 59 | 60 | But we had no ideal Mistress raising her divine form to the clouds, nor any cruel Queen to whom to offer our 61 | bodies, twisted like Byzantine rings! There was nothing to make us wish for death, unless the wish to be free at 62 | last from the weight of our courage! 63 | 64 | And on we raced, hurling watchdogs against doorsteps, curling them under our burning tires like collars 65 | under a flatiron. Death, domesticated, met me at every turn, gracefully holding out a paw, or once in a while 66 | hunkering down, making velvety caressing eyes at me from every puddle. 67 | 68 | 'Let's break out of the horrible shell of wisdom and throw ourselves like pride-ripened fruit into the wide, 69 | contorted mouth of the wind! Let's give ourselves utterly to the Unknown, not in desperation but only to 70 | replenish the deep wells of the Absurd!' 71 | 72 | The words were scarcely out of my mouth when I spun my car around with the frenzy of a dog trying to 73 | bite its tail, and there, suddenly, were two cyclists coming towards me, shaking their fists, wobbling like two 74 | equally convincing but nevertheless contradictory arguments. Their stupid dilemma was blocking my way — 75 | Damn! Ouch!... I stopped short and to my disgust rolled over into a ditch with my wheels in the air... 76 | 77 | 0 maternal ditch, almost full of muddy water! Fair factory drain! I gulped down your nourishing sludge; and I 78 | remembered the blessed black beast of my Sudanese nurse. . . When I came up — torn, filthy, and stinking — 79 | from under the capsized car, I felt the white-hot iron of joy deliciously pass through my heart! 80 | 81 | A crowd of fishermen with handlines and gouty naturalists were already swarming around the prodigy. With 82 | patient, loving care those people rigged a tall derrick and iron grapnels to fish out my car, like a big beached 83 | shark. Up it came from the ditch, slowly, leaving in the bottom, like scales, its heavy framework of good sense 84 | and its soft upholstery of comfort. 85 | 86 | They thought it was dead, my beautiful shark, but a caress from me was enough to revive it; and there it was, 87 | alive again, running on its powerful fins! 88 | 89 | And so, faces smeared with good factory muck — plastered with metallic waste, with senseless sweat, with 90 | celestial soot — we, bruised, our arms in slings, but unafraid, declared our high intentions to all the living of the 91 | earth: 92 | 93 | MANIFESTO OF FUTURISM 94 | 95 | 1 . We intend to sing the love of danger, the habit of energy and fearlessness. 96 | 97 | 2. Courage, audacity, and revolt will be essential elements of our poetry. 98 | 99 | 3. Up to now literature has exalted a pensive immobility, ecstasy, and sleep. We intend to exalt 100 | aggresive action, a feverish insomnia, the racer's stride, the mortal leap, the punch and the slap. 101 | 102 | 4. We affirm that the world's magnificence has been enriched by a new beauty: the beauty of speed. A 103 | racing car whose hood is adorned with great pipes, like serpents of explosive breath — a roaring car 104 | that seems to ride on grapeshot is more beautiful than the Victory of Samothrace. 105 | 106 | 5. We want to hymn the man at the wheel, who hurls the lance of his spirit across the Earth, along the 107 | circle of its orbit. 108 | 109 | 6. The poet must spend himself with ardor, splendor, and generosity, to swell the enthusiastic fervor of 110 | the primordial elements. 111 | 112 | 7. Except in struggle, there is no more beauty. No work without an aggressive character can be a 113 | masterpiece. Poetry must be conceived as a violent attack on unknown forces, to reduce and prostrate 114 | them before man. 115 | 116 | 8. We stand on the last promontory of the centuries! . . . Why should we look back, when what we want 117 | is to break down the mysterious doors of the Impossible? Time and Space died yesterday. We already 118 | live in the absolute, because we have created eternal, omnipresent speed. 119 | 120 | 9. We will glorify war — the world's only hygiene — militarism, patriotism, the destructive gesture of 121 | freedom-bringers, beautiful ideas worth dying for, and scorn for woman. 122 | 123 | 1 0. We will destroy the museums, libraries, academies of every kind, will fight moralism, feminism, every 124 | opportunistic or utilitarian cowardice. 125 | 126 | 127 | 128 | 11. We will sing of great crowds excited by work, by pleasure, and by riot; we will sing of the 129 | 130 | multicolored, polyphonic tides of revolution in the modern capitals; we will sing of the vibrant nightly 131 | fervor of arsenals and shipyards blazing with violent electric moons; greedy railway stations that 132 | devour smoke-plumed serpents; factories hung on clouds by the crooked lines of their smoke; bridges 133 | that stride the rivers like giant gymnasts, flashing in the sun with a glitter of knives; adventurous 134 | steamers that sniff the horizon; deep-chested locomotives whose wheels paw the tracks like the 135 | hooves of enormous steel horses bridled by tubing; and the sleek flight of planes whose propellers 136 | chatter in the wind like banners and seem to cheer like an enthusiastic crowd. 137 | 138 | It is from Italy that we launch through the world this violently upsetting incendiary manifesto of ours. With 139 | it, today, we establish Futurism, because we want to free this land from its smelly gangrene of professors, 140 | archaeologists, ciceroni and antiquarians. For too long has Italy been a dealer in second-hand clothes. We mean 141 | to free her from the numberless museums that cover her like so many graveyards. 142 | 143 | Museums: cemeteries! . . . Identical, surely, in the sinister promiscuity of so many bodies unknown to one 144 | another. Museums: public dormitories where one lies forever beside hated or unknown beings. Museums: 145 | absurd abattoirs of painters and sculptors ferociously slaughtering each other with color-blows and line-blows, 146 | the length of the fought-over walls! 147 | 148 | That one should make an annual pilgrimage, just as one goes to the graveyard on All Souls' Day — that I grant. 149 | That once a year one should leave a floral tribute beneath the Gioconda, I grant you that. . . But I don't admit 150 | that our sorrows, our fragile courage, our morbid restlessness should be given a daily conducted tour through 151 | the museums. Why poison ourselves? Why rot? 152 | 153 | And what is there to see in an old picture except the laborious contortions of an artist throwing himself against 154 | the barriers that thwart his desire to express his dream completely?. . . Admiring an old picture is the same as 155 | pouring our sensibility into a funerary urn instead of hurtling it far off, in violent spasms of action and 156 | creation. 157 | 158 | Do you, then, wish to waste all your best powers in this eternal and futile worship of the past, from which you 159 | emerge fatally exhausted, shrunken, beaten down? 160 | 161 | In truth I tell you that daily visits to museums, libraries, and academies (cemeteries of empty exertion, Calvaries 162 | of crucified dreams, registries of aborted beginnings!) are, for artists, as damaging as the prolonged supervision 163 | by parents of certain young people drunk with their talent and their ambitious wills. When the future is barred 164 | to them, the admirable past may be a solace for the ills of the moribund, the sickly, the prisoner. . . But we want 165 | no part of it, the past, we the young and strong Futurists! 166 | 167 | 168 | So let them come, the gay incendiaries with charred fingers! Here they are! Here they are! ... Come on! set fire 169 | to the library shelves! Turn aside the canals to flood the museums! ... Oh, the joy of seeing the glorious old 170 | canvases bobbing adrift on those waters, discolored and shredded! . . . Take up your pickaxes, your axes and 171 | hammers and wreck, wreck the venerable cities, pitilessly! 172 | 173 | 174 | The oldest of us is thirty: so we have at least a decade for finishing our work. When we are forty, other 175 | younger and stronger men will probably throw us in the wastebasket like useless manuscripts — we want it to 176 | happen! 177 | 178 | They will come against us, our successors, will come from far away, from every quarter, dancing to the winged 179 | cadence of their first songs, flexing the hooked claws of predators, sniffing doglike at the academy doors the 180 | strong odor of our decaying minds, which will have already been promised to the literary catacombs. 181 | 182 | But we won't be there. . . At last they'll find us — one winter's night — in open country, beneath a sad roof 183 | drummed by a monotonous rain. They'll see us crouched beside our trembling aeroplanes in the act of warming 184 | our hands at the poor little blaze that our books of today will give out when they take fire from the flight of our 185 | images. 186 | 187 | They'll storm around us, panting with scorn and anguish, and all of them, exasperated by our proud daring, will 188 | hurtle to kill us, driven by a hatred the more implacable the more their hearts will be drunk with love and 189 | admiration for us. 190 | 191 | Injustice, strong and sane, will break out radiantly in their eyes. 192 | 193 | Art, in fact, can be nothing but violence, cruelty, and injustice. 194 | 195 | 196 | 197 | The oldest of us is thirty: even so we have already scattered treasures, a thousand treasures of force, love, 198 | courage, astuteness, and raw will-power; have thrown them impatiently away, with fury, carelessly, 199 | unhesitatingly, breathless, and unresting. . . Look at us! We are still untired! Our hearts know no weariness 200 | because they are fed with fire, hatred, and speed! ... Does that amaze you? 201 | 202 | It should, because you can never remember having lived! Erect on the summit of the world, once again we hurl 203 | our defiance at the stars! You have objections? — Enough! Enough! We know them... We've 204 | understood!... Our fine deceitful intelligence tells us that we are the revival and extension of our ancestors — 205 | Perhaps! ... If only it were so! — But who cares? We don't want to understand! ... Woe to anyone who says 206 | those infamous words to us again! 207 | 208 | Lift up your heads! 209 | 210 | Erect on the summit of the world, once again we hurl defiance to the stars! 211 | -------------------------------------------------------------------------------- /tests/test_markov_novel.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | import markovify 5 | 6 | import markov_novel 7 | 8 | 9 | def test_main(): 10 | """ 11 | Basic functional test 12 | """ 13 | assert markov_novel 14 | path = 'tmp' 15 | os.makedirs(path) 16 | os.chdir(path) 17 | # Get raw text as string. 18 | from os.path import dirname, abspath 19 | filename = os.path.join( 20 | dirname(dirname(abspath(__file__))), 'tests/futuristmanifest.txt') 21 | with open(filename) as f: 22 | text = f.read() 23 | # Build the model. 24 | text_model = markovify.Text(text) 25 | novel = markov_novel.Novel(text_model, chapter_count=1) 26 | novel.write(novel_title='my-novel', filetype='md') 27 | assert os.path.exists(os.path.join(os.getcwd(), 'my-novel.md')) 28 | os.chdir(os.pardir) 29 | shutil.rmtree('tmp', ignore_errors=True) 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; a generative tox configuration, see: https://testrun.org/tox/latest/config.html#generative-envlist 2 | 3 | [tox] 4 | envlist = 5 | clean, 6 | check, 7 | {py27,py33,py34,py35,pypy}, 8 | report, 9 | docs 10 | 11 | [testenv] 12 | basepython = 13 | pypy: {env:TOXPYTHON:pypy} 14 | {py27,docs,spell}: {env:TOXPYTHON:python2.7} 15 | py33: {env:TOXPYTHON:python3.3} 16 | py34: {env:TOXPYTHON:python3.4} 17 | py35: {env:TOXPYTHON:python3.5} 18 | {clean,check,report,coveralls,codecov}: python3.5 19 | bootstrap: python 20 | setenv = 21 | PYTHONPATH={toxinidir}/tests 22 | PYTHONUNBUFFERED=yes 23 | passenv = 24 | * 25 | usedevelop = false 26 | deps = 27 | pytest 28 | pytest-travis-fold 29 | pytest-cov 30 | markovify 31 | commands = 32 | {posargs:py.test --cov --cov-report=term-missing -vv tests} 33 | 34 | [testenv:bootstrap] 35 | deps = 36 | jinja2 37 | matrix 38 | skip_install = true 39 | commands = 40 | python ci/bootstrap.py 41 | passenv = 42 | * 43 | 44 | [testenv:spell] 45 | setenv = 46 | SPELLCHECK=1 47 | commands = 48 | sphinx-build -b spelling docs dist/docs 49 | skip_install = true 50 | deps = 51 | -r{toxinidir}/docs/requirements.txt 52 | sphinxcontrib-spelling 53 | pyenchant 54 | 55 | [testenv:docs] 56 | deps = 57 | -r{toxinidir}/docs/requirements.txt 58 | commands = 59 | sphinx-build {posargs:-E} -b html docs dist/docs 60 | sphinx-build -b linkcheck docs dist/docs 61 | 62 | [testenv:check] 63 | deps = 64 | docutils 65 | check-manifest 66 | flake8 67 | readme-renderer 68 | pygments 69 | isort 70 | skip_install = true 71 | commands = 72 | python setup.py check --strict --metadata --restructuredtext 73 | check-manifest {toxinidir} 74 | flake8 src tests setup.py 75 | isort --verbose --check-only --diff --recursive src tests setup.py 76 | 77 | [testenv:coveralls] 78 | deps = 79 | coveralls 80 | skip_install = true 81 | commands = 82 | coverage combine --append 83 | coverage report 84 | coveralls [] 85 | 86 | [testenv:codecov] 87 | deps = 88 | codecov 89 | skip_install = true 90 | commands = 91 | coverage combine --append 92 | coverage report 93 | coverage xml --ignore-errors 94 | codecov [] 95 | 96 | 97 | [testenv:report] 98 | deps = coverage 99 | skip_install = true 100 | commands = 101 | coverage combine --append 102 | coverage report 103 | coverage html 104 | 105 | [testenv:clean] 106 | commands = coverage erase 107 | skip_install = true 108 | deps = coverage 109 | 110 | --------------------------------------------------------------------------------