├── .coveragerc
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── .style.yapf
├── .travis.yml
├── MANIFEST.in
├── Makefile
├── Projectfile
├── README.rst
├── RELEASE.rst
├── TODO.rst
├── bin
└── generate_apidoc.py
├── classifiers.txt
├── docs
├── Makefile
├── _static
│ ├── custom.css
│ ├── graphs.css
│ └── medikit.png
├── _templates
│ ├── alabaster
│ │ ├── __init__.py
│ │ ├── _version.py
│ │ ├── about.html
│ │ ├── donate.html
│ │ ├── layout.html
│ │ ├── navigation.html
│ │ ├── relations.html
│ │ ├── static
│ │ │ ├── alabaster.css_t
│ │ │ └── custom.css
│ │ ├── support.py
│ │ └── theme.conf
│ ├── base.html
│ ├── index.html
│ ├── layout.html
│ ├── sidebarinfos.html
│ ├── sidebarintro.html
│ └── sidebarlogo.html
├── changelog.rst
├── commands.rst
├── conf.py
├── features.rst
├── features
│ ├── _usage
│ │ ├── docker.rst
│ │ ├── git.rst
│ │ ├── kube.rst
│ │ ├── make.rst
│ │ ├── python.rst
│ │ └── sphinx.rst
│ ├── django.rst
│ ├── docker.rst
│ ├── format.rst
│ ├── git.rst
│ ├── kube.rst
│ ├── make.rst
│ ├── nodejs.rst
│ ├── pylint.rst
│ ├── pytest.rst
│ ├── python.rst
│ ├── sphinx.rst
│ ├── webpack.rst
│ └── yapf.rst
├── guide.rst
├── index.rst
├── install.rst
├── make.bat
└── overview.rst
├── medikit
├── __init__.py
├── __main__.py
├── _version.py
├── commands
│ ├── __init__.py
│ ├── base.py
│ ├── init.py
│ ├── main.py
│ ├── pipeline.py
│ ├── update.py
│ └── utils.py
├── compat.py
├── config
│ ├── __init__.py
│ ├── defaults.py
│ ├── loader.py
│ └── registry.py
├── events.py
├── feature
│ ├── __init__.py
│ ├── django.py
│ ├── docker.py
│ ├── format.py
│ ├── git.py
│ ├── kube.py
│ ├── make
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── events.py
│ │ ├── resources.py
│ │ └── utils.py
│ ├── nodejs.py
│ ├── pylint.py
│ ├── pytest.py
│ ├── python.py
│ ├── sphinx.py
│ ├── template
│ │ ├── Projectfile.j2
│ │ ├── console_script
│ │ │ └── __main__.py.j2
│ │ ├── django
│ │ │ ├── Makefile
│ │ │ ├── manage.py.j2
│ │ │ ├── settings.py.j2
│ │ │ ├── urls.py.j2
│ │ │ └── wsgi.py.j2
│ │ ├── pytest
│ │ │ ├── coveragerc.j2
│ │ │ └── travis.yml.j2
│ │ ├── python
│ │ │ ├── package_init.py.j2
│ │ │ └── setup.py.j2
│ │ └── yapf
│ │ │ └── style.yapf.j2
│ ├── webpack.py
│ └── yapf.py
├── file.py
├── globals.py
├── pipeline.py
├── resources
│ ├── __init__.py
│ └── configparser.py
├── settings.py
├── steps
│ ├── __init__.py
│ ├── base.py
│ ├── exec.py
│ ├── install.py
│ ├── utils
│ │ ├── __init__.py
│ │ └── process.py
│ └── version.py
├── structs.py
├── testing.py
└── utils.py
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── setup.cfg
├── setup.py
└── tests
├── .gitkeep
├── conftest.py
├── feature
├── test_feature_docker.py
├── test_feature_git.py
├── test_feature_pytest.py
└── test_feature_python.py
├── test_event.py
├── test_pipelines.py
└── test_utils.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 |
4 | [report]
5 | # Regexes for lines to exclude from consideration
6 | exclude_lines =
7 | # Have to re-enable the standard pragma
8 | pragma: no cover
9 |
10 | # Don't complain about missing debug-only code:
11 | def __repr__
12 | if self\.debug
13 |
14 | # Don't complain if tests don't hit defensive assertion code:
15 | raise AbstractError
16 | raise AssertionError
17 | raise NotImplementedError
18 |
19 | # Don't complain if non-runnable code isn't run:
20 | if 0:
21 | if __name__ == .__main__.:
22 |
23 | ignore_errors = True
24 |
25 | [html]
26 | directory = docs/_build/html/coverage
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.iml
3 | *.pyc
4 | .*.sw?
5 | .sw?
6 | /.cache
7 | /.coverage
8 | /.idea
9 | /.medikit
10 | /.release
11 | /.virtualenv-python*
12 | /.vscode
13 | /build
14 | /dist
15 | /docs/_build
16 | /htmlcov
17 | /pip-wheel-metadata/
18 | /pylint.html
19 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 |
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v3.2.0
5 | hooks:
6 | - id: trailing-whitespace
7 | - id: end-of-file-fixer
8 | - id: check-yaml
9 | - id: check-added-large-files
10 |
11 | - repo: https://github.com/psf/black
12 | rev: 20.8b1
13 | hooks:
14 | - id: black
15 |
16 | - repo: https://github.com/pycqa/isort
17 | rev: 5.6.4
18 | hooks:
19 | - id: isort
20 | name: isort (python)
21 | - id: isort
22 | name: isort (cython)
23 | types: [cython]
24 | - id: isort
25 | name: isort (pyi)
26 | types: [pyi]
27 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | sphinx:
4 | configuration: docs/conf.py
5 |
6 | formats: all
7 |
8 | python:
9 | version: 3.7
10 | install:
11 | - method: pip
12 | path: .
13 | extra_requirements:
14 | - dev
15 |
--------------------------------------------------------------------------------
/.style.yapf:
--------------------------------------------------------------------------------
1 | [style]
2 | based_on_style = pep8
3 | column_limit = 120
4 | dedent_closing_brackets = true
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - 3.5
4 | - 3.5-dev
5 | - 3.6
6 | - 3.6-dev
7 | - 3.7-dev
8 | - nightly
9 | - pypy3
10 | install:
11 | - make install-dev
12 | - pip install coveralls
13 | script:
14 | - make clean test
15 | after_success:
16 | - coveralls
17 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | recursive-include medikit *.j2
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Generated by Medikit 0.8.0 on 2021-09-08.
2 | # All changes will be overriden.
3 | # Edit Projectfile and run “make update” (or “medikit update”) to regenerate.
4 |
5 |
6 | PACKAGE ?= medikit
7 | PYTHON ?= $(shell which python3 || which python || echo python3)
8 | PYTHON_BASENAME ?= $(shell basename $(PYTHON))
9 | PYTHON_DIRNAME ?= $(shell dirname $(PYTHON))
10 | PYTHON_REQUIREMENTS_FILE ?= requirements.txt
11 | PYTHON_REQUIREMENTS_INLINE ?=
12 | PYTHON_REQUIREMENTS_DEV_FILE ?= requirements-dev.txt
13 | PYTHON_REQUIREMENTS_DEV_INLINE ?=
14 | QUICK ?=
15 | PIP ?= $(PYTHON) -m pip
16 | PIP_INSTALL_OPTIONS ?=
17 | VERSION ?= $(shell git describe 2>/dev/null || git rev-parse --short HEAD)
18 | BLACK ?= $(shell which black || echo black)
19 | BLACK_OPTIONS ?= --line-length 120
20 | ISORT ?= $(PYTHON) -m isort
21 | ISORT_OPTIONS ?=
22 | PYTEST ?= $(PYTHON_DIRNAME)/pytest
23 | PYTEST_OPTIONS ?= --capture=no --cov=$(PACKAGE) --cov-report html
24 | SPHINX_BUILD ?= $(PYTHON_DIRNAME)/sphinx-build
25 | SPHINX_OPTIONS ?=
26 | SPHINX_SOURCEDIR ?= docs
27 | SPHINX_BUILDDIR ?= $(SPHINX_SOURCEDIR)/_build
28 | SPHINX_AUTOBUILD ?= $(PYTHON_DIRNAME)/sphinx-autobuild
29 | MEDIKIT ?= $(PYTHON) -m medikit
30 | MEDIKIT_UPDATE_OPTIONS ?=
31 | MEDIKIT_VERSION ?= 0.8.0
32 |
33 | .PHONY: $(SPHINX_SOURCEDIR) clean format help install install-dev medikit quick release test update update-requirements watch-$(SPHINX_SOURCEDIR)
34 |
35 | install: .medikit/install ## Installs the project.
36 | .medikit/install: $(PYTHON_REQUIREMENTS_FILE) setup.py
37 | $(eval target := $(shell echo $@ | rev | cut -d/ -f1 | rev))
38 | ifeq ($(filter quick,$(MAKECMDGOALS)),quick)
39 | @printf "Skipping \033[36m%s\033[0m because of \033[36mquick\033[0m target.\n" $(target)
40 | else ifneq ($(QUICK),)
41 | @printf "Skipping \033[36m%s\033[0m because \033[36m$$QUICK\033[0m is not empty.\n" $(target)
42 | else
43 | @printf "Applying \033[36m%s\033[0m target...\n" $(target)
44 | $(PIP) install $(PIP_INSTALL_OPTIONS) -U "pip >=19,<20" wheel
45 | $(PIP) install $(PIP_INSTALL_OPTIONS) -U $(PYTHON_REQUIREMENTS_INLINE) -r $(PYTHON_REQUIREMENTS_FILE)
46 | @mkdir -p .medikit; touch $@
47 | endif
48 |
49 | clean: ## Cleans up the working copy.
50 | rm -rf build dist *.egg-info .medikit/install .medikit/install-dev
51 | find . -name __pycache__ -type d | xargs rm -rf
52 |
53 | install-dev: .medikit/install-dev ## Installs the project (with dev dependencies).
54 | .medikit/install-dev: $(PYTHON_REQUIREMENTS_DEV_FILE) setup.py
55 | $(eval target := $(shell echo $@ | rev | cut -d/ -f1 | rev))
56 | ifeq ($(filter quick,$(MAKECMDGOALS)),quick)
57 | @printf "Skipping \033[36m%s\033[0m because of \033[36mquick\033[0m target.\n" $(target)
58 | else ifneq ($(QUICK),)
59 | @printf "Skipping \033[36m%s\033[0m because \033[36m$$QUICK\033[0m is not empty.\n" $(target)
60 | else
61 | @printf "Applying \033[36m%s\033[0m target...\n" $(target)
62 | $(PIP) install $(PIP_INSTALL_OPTIONS) -U "pip >=19,<20" wheel
63 | $(PIP) install $(PIP_INSTALL_OPTIONS) -U $(PYTHON_REQUIREMENTS_DEV_INLINE) -r $(PYTHON_REQUIREMENTS_DEV_FILE)
64 | @mkdir -p .medikit; touch $@
65 | endif
66 |
67 | quick: #
68 | @printf ""
69 |
70 | format: install-dev ## Reformats the codebase (with black, isort).
71 | $(BLACK) $(BLACK_OPTIONS) . Projectfile
72 | $(ISORT) $(ISORT_OPTIONS) . Projectfile
73 |
74 | test: install-dev ## Runs the test suite.
75 | $(PYTEST) $(PYTEST_OPTIONS) tests
76 |
77 | $(SPHINX_SOURCEDIR): install-dev ##
78 | $(SPHINX_BUILD) -b html -D latex_paper_size=a4 $(SPHINX_OPTIONS) $(SPHINX_SOURCEDIR) $(SPHINX_BUILDDIR)/html
79 |
80 | watch-$(SPHINX_SOURCEDIR): ##
81 | $(SPHINX_AUTOBUILD) $(SPHINX_SOURCEDIR) $(shell mktemp -d)
82 |
83 | release: medikit ## Runs the "release" pipeline.
84 | $(MEDIKIT) pipeline release start
85 |
86 | medikit: # Checks installed medikit version and updates it if it is outdated.
87 | @$(PYTHON) -c 'import medikit, pip, sys; from packaging.version import Version; sys.exit(0 if (Version(medikit.__version__) >= Version("$(MEDIKIT_VERSION)")) and (Version(pip.__version__) < Version("10")) else 1)' || $(PYTHON) -m pip install -U "pip >=19,<20" "medikit>=$(MEDIKIT_VERSION)"
88 |
89 | update: medikit ## Update project artifacts using medikit.
90 | $(MEDIKIT) update $(MEDIKIT_UPDATE_OPTIONS)
91 |
92 | update-requirements: ## Update project artifacts using medikit, including requirements files.
93 | MEDIKIT_UPDATE_OPTIONS="--override-requirements" $(MAKE) update
94 |
95 | help: ## Shows available commands.
96 | @echo "Available commands:"
97 | @echo
98 | @grep -E '^[a-zA-Z_-]+:.*?##[\s]?.*$$' --no-filename $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?##"}; {printf " make \033[36m%-30s\033[0m %s\n", $$1, $$2}'
99 | @echo
100 |
--------------------------------------------------------------------------------
/Projectfile:
--------------------------------------------------------------------------------
1 | # medikit (see github.com/python-edgy/medikit)
2 |
3 | from medikit import listen, pipeline, require
4 | from medikit.steps.exec import System
5 |
6 | sphinx = require("sphinx")
7 |
8 | # tests
9 | with require("pytest") as pytest:
10 | pytest.set_version("~=6.0")
11 | pytest.addons["coverage"] = "~=5.3"
12 |
13 | # code formater
14 | with require("format") as fmt:
15 | fmt.using("black", "isort")
16 |
17 | with require("python") as python:
18 | python.setup(
19 | name="medikit",
20 | python_requires=">=3.5",
21 | description="Opinionated python 3.5+ project management.",
22 | license="Apache License, Version 2.0",
23 | url="https://python-medikit.github.io/",
24 | download_url="https://github.com/python-medikit/medikit/archive/{version}.tar.gz",
25 | author="Romain Dorgueil",
26 | author_email="romain@dorgueil.net",
27 | entry_points={
28 | "console_scripts": ["medikit=medikit.__main__:main"],
29 | "medikit.feature": [
30 | "django = medikit.feature.django:DjangoFeature",
31 | "docker = medikit.feature.docker:DockerFeature",
32 | "format = medikit.feature.format:FormatFeature",
33 | "git = medikit.feature.git:GitFeature",
34 | "kube = medikit.feature.kube:KubeFeature",
35 | "make = medikit.feature.make:MakeFeature",
36 | "nodejs = medikit.feature.nodejs:NodeJSFeature",
37 | "pylint = medikit.feature.pylint:PylintFeature",
38 | "pytest = medikit.feature.pytest:PytestFeature",
39 | "python = medikit.feature.python:PythonFeature",
40 | "sphinx = medikit.feature.sphinx:SphinxFeature",
41 | "webpack = medikit.feature.webpack:WebpackFeature",
42 | "yapf = medikit.feature.yapf:YapfFeature",
43 | ],
44 | },
45 | )
46 | python.add_requirements(
47 | "git-semver ~=0.3.2",
48 | "jinja2 ~=2.10",
49 | "mondrian ~=0.8",
50 | "packaging ~=20.0",
51 | "pip >=19,<20",
52 | "pip-tools ~=4.5.0",
53 | "semantic_version <2.7", # note: this version is required as it is the one used by releases
54 | "stevedore ~=3.0",
55 | "whistle ~=1.0",
56 | "yapf ~=0.20",
57 | dev=[
58 | "sphinx-sitemap ~=1.0",
59 | "releases >=1.6,<1.7",
60 | "black ==20.8b1",
61 | "pre-commit ~=2.9.0",
62 | ],
63 | )
64 |
65 |
66 | with require("make") as make:
67 | # Sphinx
68 | @listen(make.on_generate)
69 | def on_make_generate_sphinx(event):
70 | event.makefile["SPHINX_AUTOBUILD"] = "$(PYTHON_DIRNAME)/sphinx-autobuild"
71 | event.makefile.add_target(
72 | "watch-$(SPHINX_SOURCEDIR)",
73 | """
74 | $(SPHINX_AUTOBUILD) $(SPHINX_SOURCEDIR) $(shell mktemp -d)
75 | """,
76 | phony=True,
77 | )
78 |
79 | # Pipelines
80 | @listen(make.on_generate)
81 | def on_make_generate_pipelines(event):
82 | makefile = event.makefile
83 |
84 | # Releases
85 | event.makefile.add_target(
86 | "release",
87 | "$(MEDIKIT) pipeline release start",
88 | deps=("medikit",),
89 | phony=True,
90 | doc='Runs the "release" pipeline.',
91 | )
92 |
93 |
94 | with pipeline("release") as release:
95 | release.add(System("pre-commit run || true"), before="System('git add -p .', True)")
96 |
97 | # vim: ft=python:
98 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ✚ medikit ✚
2 | ===========
3 |
4 | Strongly opinionated python 3.5+ project management.
5 |
6 | .. image:: https://travis-ci.org/python-medikit/medikit.svg?branch=master
7 | :target: https://travis-ci.org/python-medikit/medikit
8 | :alt: Continuous Integration Status
9 |
10 | .. image:: https://coveralls.io/repos/github/python-medikit/medikit/badge.svg?branch=master
11 | :target: https://coveralls.io/github/python-medikit/medikit?branch=master
12 | :alt: Coverage Status
13 |
14 | .. image:: https://readthedocs.org/projects/medikit/badge/?version=latest
15 | :target: http://edgyproject.readthedocs.org/en/latest/?badge=latest
16 | :alt: Documentation Status
17 |
18 | .. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpython-medikit%2Fmedikit.svg?type=shield
19 | :target: https://app.fossa.io/projects/git%2Bgithub.com%2Fpython-medikit%2Fmedikit?ref=badge_shield
20 | :alt: License Status
21 |
22 | Medikit is the first-aid toolkit to manage your project's boilerplate, like
23 | package files, versions, config, test suite, runners, ...
24 |
25 | This package helps you create python (or not) source trees using best practices
26 | (or at least the practices we consider as best for us) in a breeze.
27 |
28 | Don't worry about setting up git, a makefile, usual project targets, unit tests
29 | framework, pip, wheels, virtualenv, code coverage, namespace packages, setup.py
30 | files ...
31 |
32 | Medikit's got you covered on all this, using one simple and fast command.
33 |
34 |
35 | Install
36 | =======
37 |
38 | Before installing the package, you must make sure that `pip` and `virtualenv`
39 | are installed and available to be used in your current environment.
40 |
41 | .. code-block:: shell
42 |
43 | pip install medikit
44 |
45 | Now, you may want to bootstrap a python package source tree.
46 |
47 | .. code-block:: shell
48 |
49 | mkdir my.awesome.pkg
50 | cd my.awesome.pkg
51 | medikit init .
52 |
53 | You're done with the bootstrap. You can now run:
54 |
55 | .. code-block:: shell
56 |
57 | make install
58 | make test
59 | git commit -m 'Damn that was fast ...'
60 |
61 | Happy?
62 |
63 |
64 | Update
65 | ======
66 |
67 | If you change the `Projectfile` content, or update the library, you will need to run
68 | the generator again.
69 |
70 | .. code-block:: shell
71 |
72 | medikit update
73 |
74 | To better control what changes are made, I suggest that you run it on a clean git
75 | repository, then look at the dofferences using:
76 |
77 | .. code-block:: shell
78 |
79 | git diff --cached
80 |
81 | You can then commit the generated changes.
82 |
83 |
84 | Gotchas
85 | =======
86 |
87 | As the headline says, we have made strong opinionated choices about how a project
88 | tree should be organized.
89 |
90 | For example, we choose to use make to provide the main project entrypoints
91 | (install, test). We also choose to use git. And pytest. And to put root package
92 | in the project root (as opposed to a src dir or something like this). Etc.
93 |
94 | For beginners, that's a good thing, because they won't have to ask themselves
95 | questions like "What should I put in setup.py ?" or "Should I create a «src»
96 | dir or not ?". For more advanced users, it can be either a good thing if you
97 | agree with our choices, or a bad one ...
98 |
99 |
100 | Architecture
101 | ============
102 |
103 | Medikit uses a single configuration file in your project, called Projectfile.
104 |
105 | This file contains all the specific of your project:
106 |
107 | * What features you wanna use.
108 | * The customizations of those features
109 | * The additional event listeners (more on this later) you need.
110 | * The eventual pipelines that you need.
111 |
112 | At its heart, medikit uses an "event dispatcher" model.
113 |
114 | An update will dispatch a "medikit.on_start" event, and features that you required
115 | can listen (react) to this event by adding jobs to run in response. They also can
116 | dispatch their own events.
117 |
118 | As a result, you'll get your projects files updated, that will be a combination of
119 | all the events listeners executed.
120 |
121 | It means two things:
122 |
123 | * Unlike usual project templates and generators, it can both bootstrap and update
124 | your project, as best practice evolves.
125 | * It's not a dependency of your project. Once it has run, you can forget it. Either
126 | you choose to maintain your project assets with it and you'll need it installed
127 | while updating, or you can remove it and just keep the generated files.
128 |
129 |
130 | F.A.Q
131 | =====
132 |
133 | * I'm using PasteScript, isn't that enough?
134 |
135 | * PasteScript with the basic_package template will only generate a very
136 | minimalistic tree, while we install a few tools and generate more boilerplate
137 | than it does. The fact is, we were using it before but still had a lot of
138 | repeated actions to do then, and the exact aim of this project is to automate
139 | the whole. Also, PasteScript cannot update a project once generated, while we
140 | do.
141 |
142 | * Should I use it?
143 |
144 | * You're a grown man, right?
145 |
146 | * Is it stable / production ready?
147 |
148 | * Not really relevant to this project, as it's more a development tool than
149 | something you'll use in operations. However, please note that on some points
150 | and until version 1.0, we will tune things and change the way it works to find
151 | the most flexible way to operate. Thus, if you relly on a specific
152 | implementation, updates may break things. The good news is that you'll be able
153 | to review changes using `git diff --cached`, and either rollback or report
154 | issues saying how much you're disappointed (and why, don't forget the why,
155 | please).
156 |
157 | * Can I contribute?
158 |
159 | * Yes, but the right vs wrong choices decision is up to us. Probably a good
160 | idea to discuss about it (in an issue for example) first.
161 |
162 | * Can you include feature «foo»?
163 |
164 | * Probably, or maybe not. Come on github issues to discuss it, if we agree on
165 | the fact this feature is good for a lot of usages, your patch will be
166 | welcome. Also, we're working on a simple way to write "feature plugins", so
167 | even if we don't agree on something, you'll be able to code and even distribute
168 | addons that make things work the way you like.
169 |
170 | * Do you support python 3?
171 |
172 | * Of course, and for quite some times we decided to only support python 3, as we
173 | think the "10 years incubation period" we just had is a sufficient maturation
174 | period to just forget about python 2.
175 |
176 |
177 | License
178 | =======
179 |
180 | .. image:: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpython-medikit%2Fmedikit.svg?type=large
181 | :target: https://app.fossa.io/projects/git%2Bgithub.com%2Fpython-medikit%2Fmedikit?ref=badge_large
182 | :alt: License Status
183 |
184 |
--------------------------------------------------------------------------------
/RELEASE.rst:
--------------------------------------------------------------------------------
1 | How to make a release?
2 | ======================
3 |
4 | Considering the main project repository is setup as "upstream" remote for git...
5 |
6 | 1. Pull and check dependencies are there.
7 |
8 | .. code-block:: shell-session
9 |
10 | git pull upstream `git rev-parse --abbrev-ref HEAD`
11 | pip install -U pip wheel twine git-semver medikit
12 |
13 | 2. Update version.txt with the new version number
14 |
15 | .. code-block:: shell-session
16 |
17 | VERSION_FILE=`python setup.py --name | sed s@\\\.@/@g`/_version.py
18 | git fetch upstream --tags
19 | echo "__version__ = '"`git semver --next-patch`"'" > $VERSION_FILE
20 | git add $VERSION_FILE
21 |
22 | And maybe update the frozen dependencies and makefile content (medikit managed projects only)
23 |
24 | .. code-block:: shell-session
25 |
26 | make update-requirements
27 |
28 | Generate a changelog...
29 |
30 | .. code-block:: shell-session
31 |
32 | git log --oneline --no-merges --pretty=format:"* %s (%an)" `git tag | tail -n 1`..
33 |
34 | And paste it to project changelog, then format a bit. Everything that only concerns non-code stuff should be removed (documentation, etc.) and maybe some commits grouped so it's more readable for an human, and more logically organized than the raw git log.
35 |
36 | .. code-block:: shell-session
37 |
38 | vim docs/changelog.rst
39 |
40 | If you have formating to do, now is the time...
41 |
42 | .. code-block:: shell-session
43 |
44 | QUICK=1 make format && git add -p .
45 |
46 | 3. Run a full test, from a clean virtualenv
47 |
48 | .. code-block:: shell
49 |
50 | make clean install test docs
51 |
52 | 4. Create the git release
53 |
54 | .. code-block:: shell
55 |
56 | git commit -m "release: "`python setup.py --version`
57 | git tag -am `python setup.py --version` `python setup.py --version`
58 |
59 | # Push to origin
60 | git push origin `git rev-parse --abbrev-ref HEAD` --tags
61 |
62 | # Push to upstream
63 | git push upstream `git rev-parse --abbrev-ref HEAD` --tags
64 |
65 | 5. (open-source) Create the distribution in a sandbox directory & upload to PyPI (multi python versions).
66 |
67 | .. code-block:: shell
68 |
69 | pip install -U twine; (VERSION=`python setup.py --version`; rm -rf .release; mkdir .release; git archive `git rev-parse $VERSION` | tar xf - -C .release; cd .release/; for v in 3.6 3.7 3.8 3.9; do pip$v install -U wheel; python$v setup.py sdist bdist_egg bdist_wheel; done; twine upload dist/*-`python setup.py --version`*)
70 |
71 | And maybe, test that the release is now installable...
72 |
73 | .. code-block:: shell
74 |
75 | (name=`python setup.py --name`; for v in 3.6 3.7 3.8 3.9; do python$v -m pip install -U virtualenv; python$v -m virtualenv -p python$v .rtest$v; cd .rtest$v; bin/pip --no-cache-dir install $name; bin/python -c "import $name; print($name.__name__, $name.__version__);"; cd ..; rm -rf .rtest$v; done; )
76 |
77 | Note that for PRERELEASES, you must add `--pre` to `pip install` arguments.
78 |
79 | .. code-block:: shell
80 |
81 | (name=`python setup.py --name`; for v in 3.6 3.7 3.8 3.9; do python$v -m pip install -U virtualenv; python$v -m virtualenv -p python$v .rtest$v; cd .rtest$v; bin/pip --no-cache-dir install --pre $name; bin/python -c "import $name; print($name.__name__, $name.__version__);"; cd ..; rm -rf .rtest$v; done; )
82 |
83 | 5. (private) Build containers, push and patch kubernetes
84 |
85 | .. code-block:: shell
86 |
87 | make release push rollout
88 |
89 |
90 | 5. (private, old gen) Deploy with capistrano
91 |
92 | .. code-block:: shell
93 |
94 | cap (pre)prod deploy
95 |
96 |
97 |
98 | *All this process is currently being migrated to "medikit pipelines" (alpha feature).*
99 |
100 | .. code-block:: shell
101 |
102 | medikit pipeline release start
103 |
104 | *Use at own risks*
105 |
--------------------------------------------------------------------------------
/TODO.rst:
--------------------------------------------------------------------------------
1 | * remove click in favor of argparse
2 | * init / update sub commands
3 | * edgy (ag, g) cli ?
--------------------------------------------------------------------------------
/bin/generate_apidoc.py:
--------------------------------------------------------------------------------
1 | import os
2 | from textwrap import dedent
3 |
4 | from jinja2 import Environment, Template
5 |
6 | from medikit.config.loader import load_feature_extensions
7 | from medikit.settings import DEFAULT_FEATURES
8 |
9 | env = Environment()
10 | env.filters["underline"] = lambda s, c: s + "\n" + c * len(s)
11 |
12 | TEMPLATE = env.from_string(
13 | """
14 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
15 |
16 | {{ title | underline('=') }}
17 |
18 | .. automodule:: {{ module }}
19 |
20 | Usage
21 | :::::
22 |
23 | {% if is_default %}
24 |
25 | The {{ title }} feature is required, and enabled by default.
26 |
27 | To get a handle to the :class:`{{ config_class }}` instance, you can:
28 |
29 | .. code-block:: python
30 |
31 | from medikit import require
32 |
33 | {{ name }} = require('{{ name }}')
34 |
35 | {% else %}
36 |
37 | To use the {{ title }}, make sure your **Projectfile** contains the following:
38 |
39 | .. code-block:: python
40 |
41 | from medikit import require
42 |
43 | {{ name }} = require('{{ name }}')
44 |
45 | The `{{ name }}` handle is a :class:`{{ config_class }}` instance, and can be used to customize the feature.
46 |
47 | {% endif %}
48 |
49 | {% if usage %}
50 |
51 | {{ usage }}
52 |
53 | {% endif %}
54 |
55 | {% if usage_file %}
56 |
57 | .. include:: _usage/{{ name }}.rst
58 |
59 | {% endif %}
60 |
61 |
62 | {% if has_custom_config -%}
63 |
64 | Configuration
65 | :::::::::::::
66 |
67 | .. autoclass:: {{ config_class }}
68 | :members:
69 | :undoc-members:
70 |
71 | {%- endif %}
72 |
73 | Implementation
74 | ::::::::::::::
75 |
76 | .. autoclass:: {{ feature_class }}
77 | :members:
78 | :undoc-members:
79 |
80 | """.strip()
81 | + "\n\n"
82 | )
83 |
84 |
85 | def main():
86 | root_path = os.path.realpath(os.path.join(os.path.dirname(os.path.join(os.getcwd(), __file__)), ".."))
87 | doc_path = os.path.join(root_path, "docs")
88 |
89 | features = load_feature_extensions()
90 |
91 | for name in sorted(features):
92 | feature = features[name]
93 | module = feature.__module__
94 | config = feature.Config
95 | config_module = config.__module__
96 | is_default = name in DEFAULT_FEATURES
97 |
98 | usage = getattr(config, "__usage__", None)
99 | if usage:
100 | usage = dedent(usage.strip("\n"))
101 |
102 | rst = TEMPLATE.render(
103 | name=name,
104 | title=feature.__name__.replace("Feature", " Feature"),
105 | module=module,
106 | has_custom_config=(module == config_module),
107 | config_class=config.__name__,
108 | feature_class=feature.__name__,
109 | usage=usage,
110 | usage_file=os.path.exists(os.path.join(doc_path, "features/_usage", name + ".rst")),
111 | is_default=is_default,
112 | )
113 |
114 | with open(os.path.join(doc_path, "features", name + ".rst"), "w+") as f:
115 | f.write(rst)
116 |
117 |
118 | if __name__ == "__main__":
119 | main()
120 |
--------------------------------------------------------------------------------
/classifiers.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-medikit/medikit/db78a4b6a912ccff818eb3428626a204e803f77c/classifiers.txt
--------------------------------------------------------------------------------
/docs/_static/custom.css:
--------------------------------------------------------------------------------
1 | svg {
2 | border: 2px solid green
3 | }
--------------------------------------------------------------------------------
/docs/_static/graphs.css:
--------------------------------------------------------------------------------
1 | .node {
2 | }
3 |
--------------------------------------------------------------------------------
/docs/_static/medikit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-medikit/medikit/db78a4b6a912ccff818eb3428626a204e803f77c/docs/_static/medikit.png
--------------------------------------------------------------------------------
/docs/_templates/alabaster/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from alabaster import _version as version
4 |
5 |
6 | def get_path():
7 | """
8 | Shortcut for users whose theme is next to their conf.py.
9 | """
10 | # Theme directory is defined as our parent directory
11 | return os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
12 |
13 |
14 | def update_context(app, pagename, templatename, context, doctree):
15 | context["alabaster_version"] = version.__version__
16 |
17 |
18 | def setup(app):
19 | # add_html_theme is new in Sphinx 1.6+
20 | if hasattr(app, "add_html_theme"):
21 | theme_path = os.path.abspath(os.path.dirname(__file__))
22 | app.add_html_theme("alabaster", theme_path)
23 | app.connect("html-page-context", update_context)
24 | return {"version": version.__version__, "parallel_read_safe": True}
25 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/_version.py:
--------------------------------------------------------------------------------
1 | __version_info__ = (0, 7, 10)
2 | __version__ = ".".join(map(str, __version_info__))
3 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/about.html:
--------------------------------------------------------------------------------
1 | {% if theme_logo %}
2 |
3 |
4 |
5 | {% if theme_logo_name|lower == 'true' %}
6 | {{ project }}
7 | {% endif %}
8 |
9 |
10 | {% else %}
11 |
12 | {% endif %}
13 |
14 | {% if theme_description %}
15 | {{ theme_description }}
16 | {% endif %}
17 |
18 | {% if theme_github_user and theme_github_repo %}
19 | {% if theme_github_button|lower == 'true' %}
20 |
21 |
23 |
24 | {% endif %}
25 | {% endif %}
26 |
27 | {% if theme_travis_button|lower != 'false' %}
28 | {% if theme_travis_button|lower == 'true' %}
29 | {% set path = theme_github_user + '/' + theme_github_repo %}
30 | {% else %}
31 | {% set path = theme_travis_button %}
32 | {% endif %}
33 |
34 |
35 |
39 |
40 |
41 | {% endif %}
42 |
43 | {% if theme_codecov_button|lower != 'false' %}
44 | {% if theme_codecov_button|lower == 'true' %}
45 | {% set path = theme_github_user + '/' + theme_github_repo %}
46 | {% else %}
47 | {% set path = theme_codecov_button %}
48 | {% endif %}
49 |
50 |
51 |
55 |
56 |
57 | {% endif %}
58 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/donate.html:
--------------------------------------------------------------------------------
1 | {% if theme_gratipay_user or theme_gittip_user %}
2 | Donate
3 |
4 | Consider supporting the authors on Gratipay :
5 |
8 |
9 | {% endif %}
10 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/layout.html:
--------------------------------------------------------------------------------
1 | {%- extends "basic/layout.html" %}
2 |
3 | {%- block extrahead %}
4 | {{ super() }}
5 |
6 | {% if theme_touch_icon %}
7 |
8 | {% endif %}
9 | {% if theme_canonical_url %}
10 |
11 | {% endif %}
12 |
13 | {% endblock %}
14 |
15 | {# Disable base theme's top+bottom related navs; we have our own in sidebar #}
16 | {%- block relbar1 %}{% endblock %}
17 | {%- block relbar2 %}{% endblock %}
18 |
19 | {# Nav should appear before content, not after #}
20 | {%- block content %}
21 | {%- if theme_fixed_sidebar|lower == 'true' %}
22 |
23 | {{ sidebar() }}
24 | {%- block document %}
25 |
26 | {%- if render_sidebar %}
27 |
28 | {%- endif %}
29 |
30 | {% block body %} {% endblock %}
31 |
32 | {%- if render_sidebar %}
33 |
34 | {%- endif %}
35 |
36 | {%- endblock %}
37 |
38 |
39 | {%- else %}
40 | {{ super() }}
41 | {%- endif %}
42 | {%- endblock %}
43 |
44 | {%- block footer %}
45 |
58 |
59 | {% if theme_github_banner|lower != 'false' %}
60 |
61 |
62 |
63 | {% endif %}
64 |
65 | {%- endblock %}
66 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/navigation.html:
--------------------------------------------------------------------------------
1 | {{ _('Navigation') }}
2 | {{ toctree(includehidden=theme_sidebar_includehidden, collapse=theme_sidebar_collapse) }}
3 | {% if theme_extra_nav_links %}
4 |
5 |
6 | {% for text, uri in theme_extra_nav_links.items() %}
7 | {{ text }}
8 | {% endfor %}
9 |
10 | {% endif %}
11 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/relations.html:
--------------------------------------------------------------------------------
1 |
2 |
Related Topics
3 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/static/custom.css:
--------------------------------------------------------------------------------
1 | /* This file intentionally left blank. */
2 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/support.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 |
3 | from pygments.style import Style
4 | from pygments.token import (Comment, Error, Generic, Keyword, Literal, Name, Number, Operator, Other, Punctuation,
5 | String, Whitespace)
6 |
7 |
8 | # Originally based on FlaskyStyle which was based on 'tango'.
9 | class Alabaster(Style):
10 | background_color = "#f8f8f8" # doesn't seem to override CSS 'pre' styling?
11 | default_style = ""
12 |
13 | styles = {
14 | # No corresponding class for the following:
15 | # Text: "", # class: ''
16 | Whitespace: "underline #f8f8f8", # class: 'w'
17 | Error: "#a40000 border:#ef2929", # class: 'err'
18 | Other: "#000000", # class 'x'
19 | Comment: "italic #8f5902", # class: 'c'
20 | Comment.Preproc: "noitalic", # class: 'cp'
21 | Keyword: "bold #004461", # class: 'k'
22 | Keyword.Constant: "bold #004461", # class: 'kc'
23 | Keyword.Declaration: "bold #004461", # class: 'kd'
24 | Keyword.Namespace: "bold #004461", # class: 'kn'
25 | Keyword.Pseudo: "bold #004461", # class: 'kp'
26 | Keyword.Reserved: "bold #004461", # class: 'kr'
27 | Keyword.Type: "bold #004461", # class: 'kt'
28 | Operator: "#582800", # class: 'o'
29 | Operator.Word: "bold #004461", # class: 'ow' - like keywords
30 | Punctuation: "bold #000000", # class: 'p'
31 | # because special names such as Name.Class, Name.Function, etc.
32 | # are not recognized as such later in the parsing, we choose them
33 | # to look the same as ordinary variables.
34 | Name: "#000000", # class: 'n'
35 | Name.Attribute: "#c4a000", # class: 'na' - to be revised
36 | Name.Builtin: "#004461", # class: 'nb'
37 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
38 | Name.Class: "#000000", # class: 'nc' - to be revised
39 | Name.Constant: "#000000", # class: 'no' - to be revised
40 | Name.Decorator: "#888", # class: 'nd' - to be revised
41 | Name.Entity: "#ce5c00", # class: 'ni'
42 | Name.Exception: "bold #cc0000", # class: 'ne'
43 | Name.Function: "#000000", # class: 'nf'
44 | Name.Property: "#000000", # class: 'py'
45 | Name.Label: "#f57900", # class: 'nl'
46 | Name.Namespace: "#000000", # class: 'nn' - to be revised
47 | Name.Other: "#000000", # class: 'nx'
48 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword
49 | Name.Variable: "#000000", # class: 'nv' - to be revised
50 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised
51 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised
52 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
53 | Number: "#990000", # class: 'm'
54 | Literal: "#000000", # class: 'l'
55 | Literal.Date: "#000000", # class: 'ld'
56 | String: "#4e9a06", # class: 's'
57 | String.Backtick: "#4e9a06", # class: 'sb'
58 | String.Char: "#4e9a06", # class: 'sc'
59 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment
60 | String.Double: "#4e9a06", # class: 's2'
61 | String.Escape: "#4e9a06", # class: 'se'
62 | String.Heredoc: "#4e9a06", # class: 'sh'
63 | String.Interpol: "#4e9a06", # class: 'si'
64 | String.Other: "#4e9a06", # class: 'sx'
65 | String.Regex: "#4e9a06", # class: 'sr'
66 | String.Single: "#4e9a06", # class: 's1'
67 | String.Symbol: "#4e9a06", # class: 'ss'
68 | Generic: "#000000", # class: 'g'
69 | Generic.Deleted: "#a40000", # class: 'gd'
70 | Generic.Emph: "italic #000000", # class: 'ge'
71 | Generic.Error: "#ef2929", # class: 'gr'
72 | Generic.Heading: "bold #000080", # class: 'gh'
73 | Generic.Inserted: "#00A000", # class: 'gi'
74 | Generic.Output: "#888", # class: 'go'
75 | Generic.Prompt: "#745334", # class: 'gp'
76 | Generic.Strong: "bold #000000", # class: 'gs'
77 | Generic.Subheading: "bold #800080", # class: 'gu'
78 | Generic.Traceback: "bold #a40000", # class: 'gt'
79 | }
80 |
--------------------------------------------------------------------------------
/docs/_templates/alabaster/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = alabaster.css
4 | pygments_style = alabaster.support.Alabaster
5 |
6 | [options]
7 | logo =
8 | logo_name = false
9 | logo_text_align = left
10 | description =
11 | description_font_style = normal
12 | github_user =
13 | github_repo =
14 | github_button = true
15 | github_banner = false
16 | github_type = watch
17 | github_count = true
18 | badge_branch = master
19 | travis_button = false
20 | codecov_button = false
21 | gratipay_user =
22 | gittip_user =
23 | analytics_id =
24 | touch_icon =
25 | canonical_url =
26 | extra_nav_links =
27 | sidebar_includehidden = true
28 | sidebar_collapse = true
29 | show_powered_by = true
30 | show_related = false
31 |
32 | gray_1 = #444
33 | gray_2 = #EEE
34 | gray_3 = #AAA
35 |
36 | pink_1 = #FCC
37 | pink_2 = #FAA
38 | pink_3 = #D52C2C
39 |
40 | base_bg = #fff
41 | base_text = #000
42 | hr_border = #B1B4B6
43 | body_bg =
44 | body_text = #3E4349
45 | body_text_align = left
46 | footer_text = #888
47 | link = #004B6B
48 | link_hover = #6D4100
49 | sidebar_header =
50 | sidebar_text = #555
51 | sidebar_link =
52 | sidebar_link_underscore = #999
53 | sidebar_search_button = #CCC
54 | sidebar_list = #000
55 | sidebar_hr =
56 | anchor = #DDD
57 | anchor_hover_fg =
58 | anchor_hover_bg = #EAEAEA
59 | table_border = #888
60 | shadow =
61 |
62 | # Admonition options
63 | ## basic level
64 | admonition_bg =
65 | admonition_border = #CCC
66 | note_bg =
67 | note_border = #CCC
68 | seealso_bg =
69 | seealso_border = #CCC
70 |
71 | ## critical level
72 | danger_bg =
73 | danger_border =
74 | danger_shadow =
75 | error_bg =
76 | error_border =
77 | error_shadow =
78 |
79 | ## normal level
80 | tip_bg =
81 | tip_border = #CCC
82 | hint_bg =
83 | hint_border = #CCC
84 | important_bg =
85 | important_border = #CCC
86 |
87 | ## warning level
88 | caution_bg =
89 | caution_border =
90 | attention_bg =
91 | attention_border =
92 | warn_bg =
93 | warn_border =
94 |
95 | topic_bg =
96 | code_highlight_bg =
97 | highlight_bg = #FAF3E8
98 | xref_border = #fff
99 | xref_bg = #FBFBFB
100 | admonition_xref_border = #fafafa
101 | admonition_xref_bg =
102 | footnote_bg = #FDFDFD
103 | footnote_border =
104 | pre_bg =
105 | narrow_sidebar_bg = #333
106 | narrow_sidebar_fg = #FFF
107 | narrow_sidebar_link =
108 | font_size = 17px
109 | caption_font_size = inherit
110 | viewcode_target_bg = #ffd
111 | code_bg = #ecf0f3
112 | code_text = #222
113 | code_hover = #EEE
114 | code_font_size = 0.9em
115 | code_font_family = 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace
116 | font_family = 'goudy old style', 'minion pro', 'bell mt', Georgia, 'Hiragino Mincho Pro', serif
117 | head_font_family = 'Garamond', 'Georgia', serif
118 | caption_font_family = inherit
119 | code_highlight = #FFC
120 | page_width = 940px
121 | sidebar_width = 220px
122 | fixed_sidebar = false
123 |
--------------------------------------------------------------------------------
/docs/_templates/base.html:
--------------------------------------------------------------------------------
1 | {%- extends "alabaster/layout.html" %}
2 |
3 |
4 | {%- block extrahead %}
5 | {{ super() }}
6 |
19 | {% endblock %}
20 |
21 | {%- block footer %}
22 | {{ relbar() }}
23 |
24 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
46 |
47 |
48 | {%- endblock %}
49 |
--------------------------------------------------------------------------------
/docs/_templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% set title = _('Medikit — Opinionated project management.') %}
4 |
5 | {% block body %}
6 |
7 |
9 |
10 |
11 |
12 | {% trans %}
13 | Medikit helps you manage your Python (3.5+) projects, using combinable features (includes python, pytest,
14 | django, nodejs, webpack, sphinx, gnu make ... or your own!).
15 | {% endtrans %}
16 |
17 |
18 | {% trans %}
19 | Unlike project generators like cookiecutter (which it does not replace), Medikit manage your project's common files during
20 | its whole lifetime.
21 | {% endtrans %}
22 |
23 |
24 |
25 | {% trans %}
26 | Medikit can help you format your code, manage your releases and versions, update requirements and their
27 | frozen counterparts in requirements*.txt, and overall, keep in one place the logic you're using on all your projects.
28 | {% endtrans %}
29 |
30 |
31 | {% trans %}Documentation{% endtrans %}
32 |
33 |
34 |
35 |
36 | {% trans %}Quick Start{% endtrans %}
37 | {% trans %}install medikit and get started{% endtrans %}
38 |
39 |
40 | {% trans %}Definitive Guide{% endtrans %}
41 | {% trans %}dive into medikit{% endtrans %}
42 |
43 |
44 |
45 |
46 | {% trans %}Reference{% endtrans %}
47 |
48 |
49 |
50 |
51 | {% trans %}Commands{% endtrans %}
52 | {% trans %}command line interface mastery{% endtrans %}
53 |
54 |
55 | {% trans %}Features{% endtrans %}
56 | {% trans %}built-in features mastery{% endtrans %}
57 |
58 |
59 |
60 |
61 | {% trans %}Indexes{% endtrans %}
62 |
63 |
64 |
65 |
66 | {% trans %}
67 | Search{% endtrans %}
68 | {% trans %}search the documentation{% endtrans %}
69 |
70 |
71 | {% trans %}Index{% endtrans %}
72 | {% trans %}general index{% endtrans %}
73 |
74 |
75 |
76 |
77 | {% endblock %}
78 |
--------------------------------------------------------------------------------
/docs/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {%- extends "base.html" %}
2 |
3 | {%- block content %}
4 | {{ relbar() }}
5 | {{ super() }}
6 | {%- endblock %}
7 |
8 |
--------------------------------------------------------------------------------
/docs/_templates/sidebarinfos.html:
--------------------------------------------------------------------------------
1 | Stay Informed
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/_templates/sidebarintro.html:
--------------------------------------------------------------------------------
1 | About Medikit
2 |
3 | Medikit helps you managing your Python (3.5+) projects, with various features. Unlike project generators like
4 | cookiecutter (wich it does not replace), Medikit is mostly used for files and assets maintenance through the project
5 | life (including things like generating releases, updating requirements, freeze requirements versions, etc.).
6 |
7 |
8 | Other Formats
9 |
10 | You can download the documentation in other formats as well:
11 |
12 |
17 |
18 | Useful Links
19 |
23 |
--------------------------------------------------------------------------------
/docs/_templates/sidebarlogo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Medikit
5 |
6 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | - :release:`0.8.0 `
5 | - :feature:`0` Format: Implements a new "format" feature that groups all code formaters, for all languages.
6 | - :feature:`0` Python: Upgraded to pip 19.x.
7 | - :feature:`0` Python: Upgraded to pip-tools 3.x.
8 | - :feature:`0` Yapf: Deprecated in favor of format that implements the same logic, but more.
9 | - :feature:`0` Add managed resource concept so some file types can be generated and overriden in a content-aware manner.
10 | - :release:`0.7.1 <2019-03-15>`
11 | - :bug:`0` Remove deserialization warning (means nothing important).
12 | - :release:`0.7.0 <2019-03-15>`
13 | - :feature:`0` Git: now less verbose.
14 | - :feature:`0` Kubernetes: basic helm support.
15 | - :feature:`0` Kubernetes: patch syntax now allows to use a dotted-string to express the "patch path".
16 | - :feature:`0` Kubernetes: supports variants to have more than one kube target.
17 | - :feature:`0` Make: Support for "header" in makefiles, allowing to prepend arbitrary directives in generated file.
18 | - :feature:`0` Make: Support for includes in makefiles (use `add_includes(...)`).
19 | - :feature:`0` Make: `set_script(...)` now allows to override a predefined make target script.
20 | - :feature:`0` Python: Support for wheelhouse (experimental, use at own risks).
21 | - :feature:`0` Python: allows to override the setup's packages option.
22 | - :feature:`0` Pipelines: Configuration object is now passed to pipeline for more flexibility.
23 | - :feature:`0` Docker: DOCKER_NAME renamed to DOCKER_RUN_NAME in case of "run" task.
24 | - :feature:`0` Uncoupling package name / version from python to use it in non-python projects.
25 | - :feature:`0` Django: Upgraded django version.
26 | - :feature:`0` Python: Upgraded to pip version 18.
27 | - :feature:`0` Misc: Upgraded various python packages.
28 | - :feature:`0` Added changelog file.
29 | - :feature:`0` Switched internal formating to black / isort instead of yapf.
30 | - :bug:`0` Fixed make help that would break in case of included submakefiles.
31 | - :release:`0.6.3 <2018-05-30>`
32 |
33 |
--------------------------------------------------------------------------------
/docs/commands.rst:
--------------------------------------------------------------------------------
1 | Commands Reference
2 | ==================
3 |
4 |
5 | Init
6 | ::::
7 |
8 | Creates a project (bootstraps the Projectfile, then run an update).
9 |
10 | .. code-block:: shell-session
11 |
12 | $ medikit init
13 |
14 |
15 | Update
16 | ::::::
17 |
18 | Updates a project, according to Projectfile.
19 |
20 | .. code-block:: shell-session
21 |
22 | $ medikit update
23 |
24 |
25 | Pipeline (alpha)
26 | ::::::::::::::::
27 |
28 | Starts, or continue, a project management pipeline.
29 |
30 | .. code-block:: shell-session
31 |
32 | $ medikit pipeline release start
33 |
34 | If the pipeline already started, you can resume it:
35 |
36 | .. code-block:: shell-session
37 |
38 | $ medikit pipeline release continue
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/docs/features.rst:
--------------------------------------------------------------------------------
1 | Features Reference
2 | ==================
3 |
4 | Features are the base building blocks of Medikit. In each project, you can "require" a bunch of features, it will
5 | tell medikit what to handle.
6 |
7 | Each feature works the same, giving you a "configuration" object when you require it. You can use this configuration
8 | object to change the default behaviour of Medikit.
9 |
10 | For example, to add a requirement to the python feature, write in your Projectfile:
11 |
12 | .. code-block:: python
13 |
14 | from medikit import require
15 |
16 | python = require('python')
17 |
18 | python.add_requirements('requests >=2,<3')
19 |
20 |
21 | For custom behaviors that goes further than just changing feature configurations, you can listen to a number of events
22 | exposed by the features.
23 |
24 | Here is an example:
25 |
26 | .. code-block:: python
27 |
28 | from medikit import require, listen
29 |
30 | make = require('make')
31 |
32 | @listen(make.on_generate)
33 | def on_make_generate(event):
34 | event.makefile.add_target(
35 | 'hello',
36 | 'echo "Hello world"',
37 | phony=True
38 | )
39 |
40 | .. toctree::
41 | :maxdepth: 1
42 |
43 | features/django
44 | features/docker
45 | features/format
46 | features/git
47 | features/kube
48 | features/make
49 | features/nodejs
50 | features/pylint
51 | features/pytest
52 | features/python
53 | features/sphinx
54 | features/webpack
55 | features/yapf
56 |
--------------------------------------------------------------------------------
/docs/features/_usage/docker.rst:
--------------------------------------------------------------------------------
1 | *This feature is brand new and should be used with care.*
2 |
3 | You'll get a few make targets out of this, and a Dockerfile (or Rockerfile).
4 |
5 | Build
6 | -----
7 |
8 | Building an image is as simple as running:
9 |
10 | .. code-block:: shell-session
11 |
12 | $ make docker-build
13 |
14 | This will use the root Dockerfile (or Rockerfile, see builders below) and build an image named after your package.
15 |
16 | Push
17 | ----
18 |
19 | You can push the built image to a docker registry, using:
20 |
21 | .. code-block:: shell-session
22 |
23 | $ make docker-push
24 |
25 | Run
26 | ---
27 |
28 | You can run the default entrypoint / command in a new container using the built image:
29 |
30 | .. code-block:: shell-session
31 |
32 | $ make docker-run
33 |
34 | Shell
35 | -----
36 |
37 | You can run a bash shell in a new container using the built image:
38 |
39 | .. code-block:: shell-session
40 |
41 | $ make docker-shell
42 |
43 | Custom builder
44 | --------------
45 |
46 | You can change the default docker builder (a.k.a "docker build") to use rocker
47 | (see https://github.com/grammarly/rocker).
48 |
49 | .. code-block:: python
50 |
51 | docker.use_rocker_builder()
52 |
53 | Custom image name or registry
54 | -----------------------------
55 |
56 | If you want to customize the image name, or the target registry (for example if you want to use Amazon Elastic
57 | Container Registry, Google Container Registry, Quay, or even a private registry you're crazy enough to host
58 | yourself):
59 |
60 | .. code-block:: python
61 |
62 | # only override the registry
63 | docker.set_remote(registry='eu.gcr.io')
64 |
65 | # override the registry and username, but keep the default image name
66 | docker.set_remote(registry='eu.gcr.io', user='sergey')
67 |
68 | # override the image name only
69 | docker.set_remote(name='acme')
70 |
71 | Docker Compose
72 | --------------
73 |
74 | The feature will also create an example docker-compose.yml file.
75 |
76 | If you don't want this, you can:
77 |
78 | .. code-block:: python
79 |
80 | docker.compose_file = None
81 |
82 | Or if you want to override its name:
83 |
84 | .. code-block:: python
85 |
86 | docker.compose_file = 'config/docker/compose.yml'
87 |
88 | Please note that this file will only contain a structure skeleton, with no service defined. This is up to you to
89 | fill, although we may work on this in the future as an opt-in managed file.
90 |
91 |
--------------------------------------------------------------------------------
/docs/features/_usage/git.rst:
--------------------------------------------------------------------------------
1 | Currently, **this feature is required for medikit to work**.
2 |
3 | To disable it, use:
4 |
5 | .. code-block:: python
6 |
7 | git.disable()
8 |
9 | It will avoid creating a `.git` repository, and won't `git add` the modified files to git neither.
10 |
11 | Even disabled, the feature will still manage the `.gitignore` file (can't harm) and the `VERSION` variable, still
12 | based on `git describe` value. It means that you can ask medikit to not create a repo, but it should still be in a
13 | sub-directory or a git repository somewhere.
14 |
15 |
--------------------------------------------------------------------------------
/docs/features/_usage/kube.rst:
--------------------------------------------------------------------------------
1 | .. warning:: This feature is brand new and should be used with care.
2 |
3 | .. todo:: Write the docs, once the feature stabilize.
4 |
--------------------------------------------------------------------------------
/docs/features/_usage/make.rst:
--------------------------------------------------------------------------------
1 | Currently, **this feature is required for medikit to work**.
2 |
3 | Makefile generation and management is quite central to medikit, and it's one of the strongest opinionated choices
4 | made by the tool.
5 |
6 | .. note:: `Makefile` will be overriden on each `medikit update` run! See below how to customize it.
7 |
8 | Everything out of the project's world is managed by a single, central Makefile, that contains all the external
9 | entrypoints to your package.
10 |
11 | By default, it only contains the following targets (a.k.a "tasks"):
12 |
13 | * install
14 | * install-dev
15 | * update
16 | * update-requirements
17 |
18 | This is highly extendable, and about all other features will add their own targets using listeners to the
19 | :obj:`MakeConfig.on_generate` event.
20 |
21 | Default targets
22 | ---------------
23 |
24 | Install
25 | .......
26 |
27 | The `make install` command will try its best to install your package in your current system environment. For python
28 | projects, it work with the system python, but you're highly encouraged to use a virtual environment (using
29 | :mod:`virtualenv` or :mod:`venv` module).
30 |
31 | .. code-block:: shell-session
32 |
33 | $ make install
34 |
35 | Install Dev
36 | ...........
37 |
38 | The `make install-dev` command works like `make install`, but adds the `dev` extra.
39 |
40 | The `dev` extra is a convention medikit takes to group all dependencies that are only required to actual hack on
41 | your project, and that won't be necessary in production / runtime environments.
42 |
43 | For python projects, it maps to an "extra", as defined by setuptools. For Node.js projects, it will use the
44 | "devDependencies".
45 |
46 | .. code-block:: shell-session
47 |
48 | $ make install-dev
49 |
50 | Update
51 | ......
52 |
53 | This is a shortcut to `medikit update`, with a preliminary dependency check on `medikit`.
54 |
55 | As you may have noticed, `medikit` is never added as a dependency to your project, so this task will ensure it's
56 | installed before running.
57 |
58 | .. code-block:: shell-session
59 |
60 | $ make update
61 |
62 | Update Requirements
63 | ...................
64 |
65 | The `make update-requirements` command works like `make update`, but forces the regeneration of `requirements*.txt`
66 | files.
67 |
68 | For security reasons, `medikit` never updates your requirements if they are already frozen in requirements files
69 | (you would not want a requirement to increment version without notice).
70 |
71 | This task is here so you can explicitely update your requirements frozen versions, according to the constraints
72 | you defined in the `Projectfile`.
73 |
74 | .. code-block:: shell-session
75 |
76 | $ make update-requirements
77 |
78 | Customize your Makefile
79 | -----------------------
80 |
81 | To customize the generated `Makefile`, you can use the same event mechanism that is used by `medikit` features,
82 | directly from within your `Projectfile`.
83 |
84 | Add a target
85 | ............
86 |
87 | .. code-block:: python
88 |
89 | from medikit import listen
90 |
91 | @listen(make.on_generate)
92 | def on_make_generate(event):
93 | event.makefile.add_target('foo', '''
94 | echo "Foo!"
95 | ''', deps=('install', ), phony=True, doc='So foo...'
96 | )
97 |
98 | This is pretty self-explanatory, but let's detail:
99 |
100 | * "foo" is the target name (you'll be able to run `make foo`)
101 | * This target will run `echo "Foo!"`
102 | * It depends on the `install` target, that needs to be satisfied (install being "phony", it will be run
103 | every time).
104 | * This task is "phony", meaning that there will be no `foo` file or directory generated as the output, and thus
105 | that `make` should consider it's never outdated.
106 | * If you create non phony targets, they must result in a matching file or directory created.
107 | * Read more about GNU Make: https://www.gnu.org/software/make/
108 |
109 | Change the dependencies of an existing target
110 | .............................................
111 |
112 | .. code-block:: python
113 |
114 | from medikit import listen
115 |
116 | @listen(make.on_generate)
117 | def on_make_generate(event):
118 | event.makefile.set_seps('foo', ('install-dev', ))
119 |
120 | Add (or override) a variable
121 | ............................
122 |
123 | .. code-block:: python
124 |
125 | from medikit import listen
126 |
127 | @listen(make.on_generate)
128 | def on_make_generate(event):
129 | event.makefile['FOO'] = 'Bar'
130 |
131 | The user can override `Makefile` variables using your system environment:
132 |
133 | .. code-block:: shell-session
134 |
135 | $ FOO=loremipsum make foo
136 |
137 | To avoid this default behaviour (which is more than ok most of the time), you can change the assignment operator
138 | used in the makefile.
139 |
140 | .. code-block:: python
141 |
142 | from medikit import listen
143 |
144 | @listen(make.on_generate)
145 | def on_make_generate(event):
146 | event.makefile.set_assignment_operator('FOO', ':=')
147 |
148 | This is an advanced feature you'll probably never need. You can `read the make variables reference
149 | `_.
150 |
--------------------------------------------------------------------------------
/docs/features/_usage/python.rst:
--------------------------------------------------------------------------------
1 | The python features makes your package real (at least if it uses python). Medikit was written originally for python,
2 | and although it's not completely true anymore, a lot of features depends on this.
3 |
4 | Setup
5 | -----
6 |
7 | You can define the setuptools' `setup(...)` arguments using `python.setup(...)`:
8 |
9 | .. code-block:: python
10 |
11 | python.setup(
12 | name='medikit',
13 | description='Opinionated python 3.5+ project management.',
14 | license='Apache License, Version 2.0',
15 | url='https://github.com/python-medikit/medikit',
16 | download_url='https://github.com/python-medikit/medikit/tarball/{version}',
17 | author='Romain Dorgueil',
18 | author_email='romain@dorgueil.net',
19 | entry_points={
20 | 'console_scripts': ['medikit=medikit.__main__:main'],
21 | }
22 | )
23 |
24 | This is required for any python package.
25 |
26 | Requirements
27 | ------------
28 |
29 | Requirements are managed using two different mechanisms:
30 |
31 | * The `setup.py` file, autogenerated and overriden by `medikit`, will contain the "loose" requirements as you define
32 | them. You're encouraged to use "~x.y.z" or "~x.y" versions. You should use each versions ("==x.y.z") only in case
33 | you're relying on a package you never want to update.
34 | * The `requirements*.txt` files will contain frozen version numbers. Those requirements will be commited, and you
35 | can ensure the reproducibility of your installs by using `pip install -r requirements.txt` instead of
36 | `python setup.py install`.
37 |
38 | In `medikit`, we call what is present in setup.py "constraints", and what is in `requirements*.txt` files
39 | "requirements".
40 |
41 | Let's see how we can set them:
42 |
43 | .. code-block:: python
44 |
45 | python.add_requirements(
46 | "requests ~2.18"
47 | )
48 |
49 | This will set a constraint on any semver compatible requests version, and update the requirement to latest requests
50 | version, compatible with the constraint (as of writing, 2.18.4).
51 |
52 | It means that if you run `make install`, `python setup.py install` or `pip install -e .`, requests will only be
53 | downloaded and installed if there is no installation complying to the constraint in your current env. This is very
54 | handy if you have local, editable packages that you want to use instead of PyPI versions.
55 |
56 | It also means that when you run `pip install -r requirements.txt`, you'll get requests 2.18.4 even if a new version
57 | was released.
58 |
59 | If you want to upgrade to the new released version, use `make update-requirements`, review the git changes
60 | (`git diff --cached`), test your software with the new version and eventually (git) commit to this dependency update.
61 |
62 | Constraints
63 | -----------
64 |
65 | Sometimes, you want a dependency to only be a constraint, and not a frozen requirement.
66 |
67 | .. code-block:: python
68 |
69 | python.add_constraints(
70 | "certifi ~2018,<2019"
71 | )
72 |
73 | This will ensure that your env contains "certifi", a version released in the 2018 year, but also says you don't care
74 | which one.
75 |
76 | This is an advanced feature that you should only use if you really know what you're doing, otherwise, use a
77 | requirement (reproducibility of installs is gold).
78 |
79 | Extras
80 | ------
81 |
82 | You can create as much "extras" as you want.
83 |
84 | As a default, medikit will create a "dev" extra, but you can add whatever you need:
85 |
86 | .. code-block:: python
87 |
88 | python.add_requirements(
89 | sql=[
90 | 'sqlalchemy ~=1.2.5',
91 | ]
92 | )
93 |
94 | The same works with constraints, of course.
95 |
96 | Changing package generation behaviour
97 | -------------------------------------
98 |
99 | Medikit creates the necessary directory structure for your package, named after your package name defined in the
100 | `python.setup()` call.
101 |
102 | If you don't want medikit to create this directory structure:
103 |
104 | .. code-block:: python
105 |
106 | python.create_packages = False
107 |
108 | Medikit also considers you'll need a version number tracking mechanism for your project. It creates a `_version.py`
109 | file in your package's root directory. To override this file's name:
110 |
111 | .. code-block:: python
112 |
113 | python.version_file = 'my_version.py'
114 |
--------------------------------------------------------------------------------
/docs/features/_usage/sphinx.rst:
--------------------------------------------------------------------------------
1 | You can customize the theme:
2 |
3 | .. code-block:: python
4 |
5 | sphinx.theme = 'sphinx_rtd_theme'
6 |
7 | Note that this should be a parsable requirement, and it won't be added to your docs/conf.py automatically.
8 |
9 | This feature will add the necessary requirements to the python feature (sphinx mostly, and eventually your theme requirement) and setup the correct makefile task to build the html docs. Note that it won't bootstrap your sphinx config file, and you still need to run the following if your documentation does not exist yet:
10 |
11 | .. code-block:: shell-session
12 |
13 | $ make update-requirements
14 | $ make install-dev
15 | $ mkdir docs
16 | $ cd docs
17 | $ sphinx-quickstart .
18 |
19 | Then, eventually tune the configuration.
20 |
21 | .. note::
22 |
23 | In the future, we may consider generating it for you if it does not exist, but it's not a priority.
24 |
25 |
--------------------------------------------------------------------------------
/docs/features/django.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Django Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.django
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Django Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | django = require('django')
20 |
21 | The `django` handle is a :class:`DjangoConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 | This will add a few items to your Makefile, and ensure a minimalistic django project structure is available.
29 |
30 | By default, it will use Django ~=2.0,<2.2, but you can tune that:
31 |
32 | .. code-block:: python
33 |
34 | django.version = '~=2.0.3'
35 |
36 | This feature will also add or extend a "prod" python extra, that will install gunicorn.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Configuration
46 | :::::::::::::
47 |
48 | .. autoclass:: DjangoConfig
49 | :members:
50 | :undoc-members:
51 |
52 | Implementation
53 | ::::::::::::::
54 |
55 | .. autoclass:: DjangoFeature
56 | :members:
57 | :undoc-members:
58 |
--------------------------------------------------------------------------------
/docs/features/docker.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Docker Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.docker
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Docker Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | docker = require('docker')
20 |
21 | The `docker` handle is a :class:`DockerConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | .. include:: _usage/docker.rst
30 |
31 |
32 |
33 |
34 | Configuration
35 | :::::::::::::
36 |
37 | .. autoclass:: DockerConfig
38 | :members:
39 | :undoc-members:
40 |
41 | Implementation
42 | ::::::::::::::
43 |
44 | .. autoclass:: DockerFeature
45 | :members:
46 | :undoc-members:
47 |
--------------------------------------------------------------------------------
/docs/features/format.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Format Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.format
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Format Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | format = require('format')
20 |
21 | The `format` handle is a :class:`FormatConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Configuration
31 | :::::::::::::
32 |
33 | .. autoclass:: FormatConfig
34 | :members:
35 | :undoc-members:
36 |
37 | Implementation
38 | ::::::::::::::
39 |
40 | .. autoclass:: FormatFeature
41 | :members:
42 | :undoc-members:
43 |
--------------------------------------------------------------------------------
/docs/features/git.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Git Feature
4 | ===========
5 |
6 | .. automodule:: medikit.feature.git
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | The Git Feature feature is required, and enabled by default.
14 |
15 | To get a handle to the :class:`GitConfig` instance, you can:
16 |
17 | .. code-block:: python
18 |
19 | from medikit import require
20 |
21 | git = require('git')
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | .. include:: _usage/git.rst
30 |
31 |
32 |
33 |
34 | Configuration
35 | :::::::::::::
36 |
37 | .. autoclass:: GitConfig
38 | :members:
39 | :undoc-members:
40 |
41 | Implementation
42 | ::::::::::::::
43 |
44 | .. autoclass:: GitFeature
45 | :members:
46 | :undoc-members:
47 |
--------------------------------------------------------------------------------
/docs/features/kube.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Kube Feature
4 | ============
5 |
6 | .. automodule:: medikit.feature.kube
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Kube Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | kube = require('kube')
20 |
21 | The `kube` handle is a :class:`KubeConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | .. include:: _usage/kube.rst
30 |
31 |
32 |
33 |
34 | Configuration
35 | :::::::::::::
36 |
37 | .. autoclass:: KubeConfig
38 | :members:
39 | :undoc-members:
40 |
41 | Implementation
42 | ::::::::::::::
43 |
44 | .. autoclass:: KubeFeature
45 | :members:
46 | :undoc-members:
47 |
--------------------------------------------------------------------------------
/docs/features/make.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Make Feature
4 | ============
5 |
6 | .. automodule:: medikit.feature.make
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | The Make Feature feature is required, and enabled by default.
14 |
15 | To get a handle to the :class:`MakeConfig` instance, you can:
16 |
17 | .. code-block:: python
18 |
19 | from medikit import require
20 |
21 | make = require('make')
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | .. include:: _usage/make.rst
30 |
31 |
32 |
33 |
34 |
35 |
36 | Implementation
37 | ::::::::::::::
38 |
39 | .. autoclass:: MakeFeature
40 | :members:
41 | :undoc-members:
42 |
--------------------------------------------------------------------------------
/docs/features/nodejs.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | NodeJS Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.nodejs
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the NodeJS Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | nodejs = require('nodejs')
20 |
21 | The `nodejs` handle is a :class:`NodeJSConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Configuration
31 | :::::::::::::
32 |
33 | .. autoclass:: NodeJSConfig
34 | :members:
35 | :undoc-members:
36 |
37 | Implementation
38 | ::::::::::::::
39 |
40 | .. autoclass:: NodeJSFeature
41 | :members:
42 | :undoc-members:
43 |
--------------------------------------------------------------------------------
/docs/features/pylint.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Pylint Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.pylint
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Pylint Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | pylint = require('pylint')
20 |
21 | The `pylint` handle is a :class:`Config` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Implementation
33 | ::::::::::::::
34 |
35 | .. autoclass:: PylintFeature
36 | :members:
37 | :undoc-members:
38 |
--------------------------------------------------------------------------------
/docs/features/pytest.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Pytest Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.pytest
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Pytest Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | pytest = require('pytest')
20 |
21 | The `pytest` handle is a :class:`PytestConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Configuration
31 | :::::::::::::
32 |
33 | .. autoclass:: PytestConfig
34 | :members:
35 | :undoc-members:
36 |
37 | Implementation
38 | ::::::::::::::
39 |
40 | .. autoclass:: PytestFeature
41 | :members:
42 | :undoc-members:
43 |
--------------------------------------------------------------------------------
/docs/features/python.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Python Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.python
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Python Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | python = require('python')
20 |
21 | The `python` handle is a :class:`PythonConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | .. include:: _usage/python.rst
30 |
31 |
32 |
33 |
34 | Configuration
35 | :::::::::::::
36 |
37 | .. autoclass:: PythonConfig
38 | :members:
39 | :undoc-members:
40 |
41 | Implementation
42 | ::::::::::::::
43 |
44 | .. autoclass:: PythonFeature
45 | :members:
46 | :undoc-members:
47 |
--------------------------------------------------------------------------------
/docs/features/sphinx.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Sphinx Feature
4 | ==============
5 |
6 | .. automodule:: medikit.feature.sphinx
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Sphinx Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | sphinx = require('sphinx')
20 |
21 | The `sphinx` handle is a :class:`SphinxConfig` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | .. include:: _usage/sphinx.rst
30 |
31 |
32 |
33 |
34 | Configuration
35 | :::::::::::::
36 |
37 | .. autoclass:: SphinxConfig
38 | :members:
39 | :undoc-members:
40 |
41 | Implementation
42 | ::::::::::::::
43 |
44 | .. autoclass:: SphinxFeature
45 | :members:
46 | :undoc-members:
47 |
--------------------------------------------------------------------------------
/docs/features/webpack.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Webpack Feature
4 | ===============
5 |
6 | .. automodule:: medikit.feature.webpack
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Webpack Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | webpack = require('webpack')
20 |
21 | The `webpack` handle is a :class:`Config` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Implementation
33 | ::::::::::::::
34 |
35 | .. autoclass:: WebpackFeature
36 | :members:
37 | :undoc-members:
38 |
--------------------------------------------------------------------------------
/docs/features/yapf.rst:
--------------------------------------------------------------------------------
1 | .. This file is auto-generated (see bin/generate_apidoc.py), do not change it manually, your changes would be overriden.
2 |
3 | Yapf Feature
4 | ============
5 |
6 | .. automodule:: medikit.feature.yapf
7 |
8 | Usage
9 | :::::
10 |
11 |
12 |
13 | To use the Yapf Feature, make sure your **Projectfile** contains the following:
14 |
15 | .. code-block:: python
16 |
17 | from medikit import require
18 |
19 | yapf = require('yapf')
20 |
21 | The `yapf` handle is a :class:`Config` instance, and can be used to customize the feature.
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Implementation
33 | ::::::::::::::
34 |
35 | .. autoclass:: YapfFeature
36 | :members:
37 | :undoc-members:
38 |
--------------------------------------------------------------------------------
/docs/guide.rst:
--------------------------------------------------------------------------------
1 | Guides
2 | ======
3 |
4 | This is a work in progress, and I'm afraid this section lacks content.
5 |
6 | Meanwhile, you can read the :doc:`features`.
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: overview.rst
2 |
3 | Documentation
4 | :::::::::::::
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | install
10 | guide
11 | commands
12 | features
13 |
14 | Indices and tables
15 | ::::::::::::::::::
16 |
17 | * :ref:`genindex`
18 | * :ref:`modindex`
19 | * :ref:`search`
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/install.rst:
--------------------------------------------------------------------------------
1 | Quick Start
2 | ===========
3 |
4 | Medikit is a release engineering tool provided as an eventual dependency, which means that using it won't make it
5 | a dependency of your project. You just need to install it to update the project assets, that will be statically updated
6 | and commited within your source control system.
7 |
8 | Users and developpers that don't need to update the project assets wont need it, and won't even need to know it was
9 | used. This also means that you can use it for a while, and stop using it if you think it does not suit your needs
10 | anymore. Or you can easily add its usage to an existing project.
11 |
12 |
13 | Installation
14 | ::::::::::::
15 |
16 | Use the pip, luke:
17 |
18 | .. code-block:: shell-session
19 |
20 | $ pip install medikit
21 |
22 | First Steps
23 | :::::::::::
24 |
25 | Bootstrap a project:
26 |
27 | .. code-block:: shell-session
28 |
29 | $ medikit init myproject
30 | $ cd myproject
31 | $ make install
32 |
33 | Edit the configuration:
34 |
35 | .. code-block:: shell-session
36 |
37 | $ vim Projectfile
38 |
39 | Update a project's assets:
40 |
41 | .. code-block:: shell-session
42 |
43 | $ medikit update
44 |
45 | ... or ...
46 |
47 | .. code-block:: shell-session
48 |
49 | $ make update
50 |
51 | To regenerate "requirements", use:
52 |
53 | .. code-block:: shell-session
54 |
55 | $ make update-requirements
56 |
--------------------------------------------------------------------------------
/docs/overview.rst:
--------------------------------------------------------------------------------
1 | Project Overview
2 | ================
3 |
4 | .. todo:: elevator pitch here
5 |
6 |
--------------------------------------------------------------------------------
/medikit/__init__.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | from contextlib import contextmanager
3 |
4 | import piptools
5 |
6 | from medikit._version import __version__
7 |
8 | on_start = "medikit.on_start"
9 | on_end = "medikit.on_end"
10 |
11 |
12 | def listen(event_id, priority=0):
13 | """
14 | This is a stub that will be replaced during medikit execution by a real implementation.
15 | A fake implementation is provided here so that IDEs understand better the Projectfile content.
16 |
17 | See `whistle.dispatcher.EventDispatcher`.
18 |
19 | """
20 |
21 | def wrapper(listener):
22 | return listener
23 |
24 | return wrapper
25 |
26 |
27 | def require(*args):
28 | """
29 | This is a stub that will be replaced during medikit execution by a real implementation.
30 | A fake implementation is provided here so that IDEs understand better the Projectfile content.
31 |
32 | See `medikit.config.registry.ConfigurationRegistry.require`.
33 |
34 | """
35 | return args[0] if len(args) == 1 else args
36 |
37 |
38 | @contextmanager
39 | def pipeline(name):
40 | """
41 | This is a stub that will be replaced during medikit execution by a real implementation.
42 | A fake implementation is provided here so that IDEs understand better the Projectfile content.
43 |
44 | See `medikit.config.registry.ConfigurationRegistry.pipeline`.
45 |
46 | """
47 | yield
48 |
49 |
50 | def words(*args, prefix=None, separator=" ", **kwargs):
51 | """
52 | Join words together using a default space separator.
53 | """
54 | return separator.join("".join(filter(None, (prefix, arg))) for arg in args).format(**kwargs)
55 |
56 |
57 | def lines(*args, prefix=None, separator="\n", **kwargs):
58 | """
59 | Join lines together using a default line feed separator.
60 | """
61 | return words(*args, prefix=prefix, separator=separator, **kwargs)
62 |
63 |
64 | def which(cmd, *more_cmds):
65 | return (
66 | "$(shell "
67 | + " || ".join(
68 | (
69 | "which {cmd}".format(cmd=cmd),
70 | *("which {cmd}".format(cmd=more_cmd) for more_cmd in more_cmds),
71 | "echo {cmd}".format(cmd=cmd),
72 | )
73 | )
74 | + ")"
75 | )
76 |
77 |
78 | __all__ = [__version__]
79 |
--------------------------------------------------------------------------------
/medikit/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import logging
4 | import os
5 | import sys
6 | import warnings
7 | from subprocess import check_output
8 |
9 | import mondrian
10 |
11 | import medikit
12 | from medikit.commands.main import MedikitCommand
13 |
14 |
15 | def main(args=None):
16 | if not sys.warnoptions:
17 | logging.captureWarnings(True)
18 | warnings.simplefilter("default", DeprecationWarning)
19 | mondrian.setup(excepthook=True)
20 | logger = logging.getLogger()
21 | logger.setLevel(logging.INFO)
22 | logging.getLogger("pip._vendor.cachecontrol.controller").setLevel(logging.ERROR)
23 |
24 | cli = MedikitCommand()
25 |
26 | options, more_args = cli.parser.parse_known_args(args if args is not None else sys.argv[1:])
27 | if options.verbose:
28 | logger.setLevel(logging.DEBUG)
29 |
30 | options = vars(options)
31 | command, handler = options.pop("command"), options.pop("_handler")
32 |
33 | config_filename = os.path.join(os.getcwd(), options.pop("target", "."), options.pop("config"))
34 |
35 | version = medikit.__version__
36 | try:
37 | if os.path.exists(os.path.join(os.path.dirname(os.path.dirname(medikit.__file__)), ".git")):
38 | try:
39 | version = (
40 | check_output(["git", "describe"], cwd=os.path.dirname(os.path.dirname(medikit.__file__)))
41 | .decode("utf-8")
42 | .strip()
43 | + " (git)"
44 | )
45 | except:
46 | version = (
47 | check_output(["git", "rev-parse", "HEAD"], cwd=os.path.dirname(os.path.dirname(medikit.__file__)))
48 | .decode("utf-8")
49 | .strip()[0:7]
50 | + " (git)"
51 | )
52 | except:
53 | warnings.warn("Git repository found, but could not find version number from the repository.")
54 |
55 | print(mondrian.term.lightwhite_bg(mondrian.term.red(" ✚ Medikit v." + version + " ✚ ")))
56 |
57 | if len(more_args):
58 | return handler(config_filename, more=more_args, **options)
59 | else:
60 | return handler(config_filename, **options)
61 |
62 |
63 | if __name__ == "__main__":
64 | main()
65 |
--------------------------------------------------------------------------------
/medikit/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.8.0"
2 |
--------------------------------------------------------------------------------
/medikit/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from medikit.commands.init import InitCommand
2 | from medikit.commands.pipeline import PipelineCommand
3 | from medikit.commands.update import UpdateCommand
4 |
5 | # XXX deprecated aliases
6 | handle_init = InitCommand.handle
7 | handle_pipeline = PipelineCommand.handle
8 | handle_update = UpdateCommand.handle
9 |
--------------------------------------------------------------------------------
/medikit/commands/base.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | from contextlib import contextmanager
4 |
5 |
6 | class CommandGroup:
7 | def __init__(self, subparsers):
8 | self.subparsers = subparsers
9 | self.children = {}
10 |
11 | def add(self, name, command=None, **kwargs):
12 | child_parser = self.subparsers.add_parser(name, **kwargs)
13 | self.children[name] = (command or Command)(child_parser)
14 | return self.children[name]
15 |
16 |
17 | class Command:
18 | @property
19 | def logger(self):
20 | try:
21 | return self._logger
22 | except AttributeError:
23 | self._logger = logging.getLogger(type(self).__name__)
24 | return self._logger
25 |
26 | def __init__(self, parser=None):
27 | parser = parser or argparse.ArgumentParser()
28 | parser.set_defaults(_handler=self.handle)
29 | self.parser = parser
30 | self.children = {}
31 | self.add_arguments(parser)
32 |
33 | def add_arguments(self, parser):
34 | """
35 | Entry point for subclassed commands to add custom arguments.
36 | """
37 | pass
38 |
39 | def handle(self, *args, **options):
40 | """
41 | The actual logic of the command. Subclasses must implement this method.
42 | """
43 | raise NotImplementedError("Subclasses of {} must provide a handle() method".format(Command.__name__))
44 |
45 | @contextmanager
46 | def create_child(self, dest, *, required=False):
47 | subparsers = self.parser.add_subparsers(dest=dest)
48 | subparsers.required = required
49 | self.children[dest] = CommandGroup(subparsers)
50 | yield self.children[dest]
51 |
--------------------------------------------------------------------------------
/medikit/commands/init.py:
--------------------------------------------------------------------------------
1 | import os
2 | from contextlib import contextmanager
3 |
4 | from medikit.commands.base import Command
5 | from medikit.events import LoggingDispatcher
6 | from medikit.feature import ProjectInitializer
7 |
8 |
9 | @contextmanager
10 | def _change_working_directory(path):
11 | old_dir = os.getcwd()
12 | os.chdir(path)
13 | try:
14 | yield
15 | finally:
16 | os.chdir(old_dir)
17 |
18 |
19 | class InitCommand(Command):
20 | def add_arguments(self, parser):
21 | parser.add_argument("target")
22 | parser.add_argument("--name")
23 | parser.add_argument("--description")
24 | parser.add_argument("--license")
25 | parser.add_argument("--feature", "-f", action="append", dest="features")
26 |
27 | @staticmethod
28 | def handle(config_filename, **options):
29 | if os.path.exists(config_filename):
30 | raise IOError(
31 | "No config should be present in current directory to initialize (found {})".format(config_filename)
32 | )
33 |
34 | config_dirname = os.path.dirname(config_filename)
35 | if not os.path.exists(config_dirname):
36 | os.makedirs(config_dirname)
37 |
38 | # Fast and dirty implementation
39 | # TODO
40 | # - input validation
41 | # - getting input from env/git conf (author...),
42 | # - dispatching something in selected features so maybe they can suggest deps
43 | # - deps selection
44 | # - ...
45 | with _change_working_directory(config_dirname):
46 | dispatcher = LoggingDispatcher()
47 | initializer = ProjectInitializer(dispatcher, options)
48 | initializer.execute()
49 | from medikit.commands import UpdateCommand
50 |
51 | return UpdateCommand.handle(config_filename)
52 |
--------------------------------------------------------------------------------
/medikit/commands/main.py:
--------------------------------------------------------------------------------
1 | from medikit.commands import InitCommand, PipelineCommand, UpdateCommand
2 | from medikit.commands.base import Command
3 |
4 |
5 | class MedikitCommand(Command):
6 | def add_arguments(self, parser):
7 | parser.add_argument("--config", "-c", default="Projectfile")
8 | parser.add_argument("--verbose", "-v", action="store_true", default=False)
9 |
10 | with self.create_child("command", required=True) as actions:
11 | # todo aliases for update/init
12 | # warning: http://bugs.python.org/issue9234
13 | actions.add("init", InitCommand, help="Create an empty project.")
14 | actions.add("update", UpdateCommand, help="Update current project.")
15 | actions.add("pipeline", PipelineCommand, help="Execute multi-steps pipelines (release, etc.).")
16 |
--------------------------------------------------------------------------------
/medikit/commands/pipeline.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 |
4 | from mondrian import term
5 |
6 | from medikit.commands.base import Command
7 | from medikit.commands.utils import _read_configuration
8 | from medikit.events import LoggingDispatcher
9 | from medikit.pipeline import ConfiguredPipeline, get_identity, logger
10 |
11 | START = "start"
12 | CONTINUE = "continue"
13 | ABORT = "abort"
14 | SHOW = "show"
15 | COMPLETE = "complete"
16 | QUIT = "quit"
17 |
18 |
19 | class PipelineCommand(Command):
20 | def add_arguments(self, parser):
21 | parser.add_argument("pipeline", default=None, nargs="?")
22 | parser.add_argument("action", choices=(START, CONTINUE, ABORT, SHOW), nargs="?")
23 | parser.add_argument("--force", "-f", action="store_true")
24 |
25 | @classmethod
26 | def handle(cls, config_filename, *, pipeline, action, force=False, verbose=False):
27 | dispatcher = LoggingDispatcher()
28 | variables, features, files, config = _read_configuration(dispatcher, config_filename)
29 |
30 | if not pipeline:
31 | raise ValueError(
32 | "You must choose a pipeline to run. Available choices: {}.".format(
33 | ", ".join(sorted(config.pipelines.keys()))
34 | )
35 | )
36 |
37 | if not pipeline in config.pipelines:
38 | raise ValueError(
39 | "Undefined pipeline {!r}. Valid choices are: {}.".format(
40 | pipeline, ", ".join(sorted(config.pipelines.keys()))
41 | )
42 | )
43 |
44 | pipeline = ConfiguredPipeline(pipeline, config.pipelines[pipeline], config)
45 | path = os.path.dirname(config_filename)
46 | pipeline_file = os.path.join(path, ".medikit/pipelines", pipeline.name + ".json")
47 | pipeline_dirname = os.path.dirname(pipeline_file)
48 | if not os.path.exists(pipeline_dirname):
49 | os.makedirs(pipeline_dirname)
50 | elif not os.path.isdir(pipeline_dirname):
51 | raise NotADirectoryError(
52 | 'The pipeline state path "{}" was found but is not a directory...'.format(pipeline_dirname)
53 | )
54 |
55 | if not action:
56 | raise RuntimeError("Choose a pipeline action: start, continue, abort.")
57 |
58 | while action:
59 | if action == SHOW:
60 | action = cls._handle_show(pipeline, filename=pipeline_file)
61 | elif action == START:
62 | action = cls._handle_start(pipeline, filename=pipeline_file, force=force)
63 | elif action == CONTINUE:
64 | action = cls._handle_continue(pipeline, filename=pipeline_file)
65 | elif action == ABORT:
66 | action = cls._handle_abort(pipeline, filename=pipeline_file)
67 | elif action == COMPLETE:
68 | target = os.path.join(
69 | ".medikit/pipelines",
70 | pipeline.name + "." + str(datetime.datetime.now()).replace(":", ".").replace(" ", ".") + ".json",
71 | )
72 | os.rename(pipeline_file, os.path.join(path, target))
73 | logger.info("Pipeline complete. State saved as “{}”.".format(target))
74 | break
75 | elif action == QUIT:
76 | break
77 | else:
78 | raise ValueError("Invalid action “{}”.".format(action))
79 | force = False
80 |
81 | @classmethod
82 | def _handle_show(cls, pipeline, *, filename):
83 | for step in pipeline:
84 | print(get_identity(step))
85 | return QUIT
86 |
87 | @classmethod
88 | def _handle_start(cls, pipeline, *, filename, force=False):
89 | if os.path.exists(filename):
90 | if not force:
91 | raise FileExistsError(
92 | "Already started, use `medikit pipeline {name} start --force` to force a restart, or use `medikit pipeline {name} continue`.".format(
93 | name=pipeline.name
94 | )
95 | )
96 | os.unlink(filename)
97 |
98 | # Initialize pipeline state to "just started".
99 | pipeline.init()
100 |
101 | # Write the new, empty state file
102 | with open(filename, "w+") as f:
103 | f.write(pipeline.serialize())
104 |
105 | # "Continue", until step 1.
106 | return CONTINUE
107 |
108 | @classmethod
109 | def _handle_continue(cls, pipeline, *, filename):
110 | if not os.path.exists(filename):
111 | raise FileNotFoundError(
112 | "Pipeline “{}” not started, hence you cannot “continue” it. Are you looking for `medikit pipeline {name} start`?".format(
113 | name=pipeline.name
114 | )
115 | )
116 |
117 | # XXX TODO add a lock file during the step and unlock at the end.
118 | with open(filename) as f:
119 | pipeline.unserialize(f.read())
120 |
121 | try:
122 | step = pipeline.next()
123 | name, current, size, descr = pipeline.name, pipeline.current, len(pipeline), str(step)
124 | logger.info(
125 | term.black(" » ").join(
126 | (
127 | term.lightblue("{} ({}/{})".format(name.upper(), current, size)),
128 | term.yellow("⇩ BEGIN"),
129 | term.lightblack(descr),
130 | )
131 | )
132 | )
133 | step.run(pipeline.meta)
134 | if step.complete:
135 | logger.info(
136 | term.black(" » ").join(
137 | (term.lightblue("{} ({}/{})".format(name.upper(), current, size)), term.green("SUCCESS"))
138 | )
139 | + "\n"
140 | )
141 | else:
142 | logger.info(
143 | term.black(" » ").join(
144 | (term.lightblue("{} ({}/{})".format(name.upper(), current, size)), term.red("FAILED"))
145 | )
146 | + "\n"
147 | )
148 | return
149 |
150 | except StopIteration:
151 | return COMPLETE
152 |
153 | with open(filename, "w+") as f:
154 | f.write(pipeline.serialize())
155 |
156 | return CONTINUE
157 |
158 | @classmethod
159 | def _handle_abort(cls, pipeline, *, filename):
160 | assert os.path.exists(filename)
161 | try:
162 | with open(filename) as f:
163 | pipeline.unserialize(f.read())
164 | pipeline.abort()
165 | finally:
166 | os.unlink(filename)
167 |
--------------------------------------------------------------------------------
/medikit/commands/update.py:
--------------------------------------------------------------------------------
1 | from mondrian import term
2 |
3 | import medikit
4 | from medikit.commands.base import Command
5 | from medikit.commands.utils import _read_configuration
6 | from medikit.config.loader import load_feature_extensions
7 | from medikit.events import LoggingDispatcher, ProjectEvent
8 |
9 |
10 | def write_resources(event):
11 | pass
12 |
13 |
14 | class UpdateCommand(Command):
15 | def add_arguments(self, parser):
16 | parser.add_argument("--override-requirements", action="store_true")
17 |
18 | @staticmethod
19 | def handle(config_filename, **kwargs):
20 | import logging
21 |
22 | logger = logging.getLogger()
23 |
24 | dispatcher = LoggingDispatcher()
25 |
26 | variables, features, files, config = _read_configuration(dispatcher, config_filename)
27 |
28 | # This is a hack, but we'd need a flexible option parser which requires too much work as of today.
29 | if kwargs.pop("override_requirements", False):
30 | if "python" in config:
31 | config["python"].override_requirements = True
32 |
33 | feature_instances = {}
34 | logger.info(
35 | "Updating {} with {} features".format(
36 | term.bold(config.package_name),
37 | ", ".join(term.bold(term.green(feature_name)) for feature_name in sorted(features)),
38 | )
39 | )
40 |
41 | all_features = load_feature_extensions()
42 |
43 | sorted_features = sorted(features) # sort to have a predictable display order
44 | for feature_name in sorted_features:
45 | logger.debug('Initializing feature "{}"...'.format(term.bold(term.green(feature_name))))
46 | try:
47 | feature = all_features[feature_name]
48 | except KeyError as exc:
49 | logger.exception('Feature "{}" not found.'.format(feature_name))
50 |
51 | if feature:
52 | feature_instances[feature_name] = feature(dispatcher)
53 |
54 | for req in feature_instances[feature_name].requires:
55 | if not req in sorted_features:
56 | raise RuntimeError('Unmet dependency: "{}" requires "{}".'.format(feature_name, req))
57 |
58 | for con in feature_instances[feature_name].conflicts:
59 | if con in sorted_features:
60 | raise RuntimeError(
61 | 'Conflicting dependency: "{}" conflicts with "{}".'.format(con, feature_name)
62 | )
63 | else:
64 | raise RuntimeError("Required feature {} not found.".format(feature_name))
65 |
66 | event = ProjectEvent(config=config)
67 | event.variables, event.files = variables, files
68 |
69 | # todo: add listener dump list in debug/verbose mode ?
70 |
71 | event = dispatcher.dispatch(medikit.on_start, event)
72 |
73 | dispatcher.dispatch(medikit.on_end, event)
74 |
75 | logger.info("Done.")
76 |
--------------------------------------------------------------------------------
/medikit/commands/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections import OrderedDict
3 |
4 | from medikit.config import read_configuration
5 | from medikit.settings import DEFAULT_FEATURES, DEFAULT_FILES
6 |
7 |
8 | def _read_configuration(dispatcher, config_filename):
9 | """
10 | Prepare the python context and delegate to the real configuration reader (see config.py)
11 |
12 | :param EventDispatcher dispatcher:
13 | :return tuple: (variables, features, files, config)
14 | """
15 | if not os.path.exists(config_filename):
16 | raise IOError("Could not find project description file (looked in {})".format(config_filename))
17 |
18 | variables = dict(PACKAGE=None)
19 |
20 | files = {filename: "" for filename in DEFAULT_FILES}
21 | features = set(DEFAULT_FEATURES)
22 |
23 | return read_configuration(dispatcher, config_filename, variables, features, files)
24 |
--------------------------------------------------------------------------------
/medikit/compat.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import warnings
3 | from contextlib import contextmanager
4 |
5 | from packaging import version
6 |
7 | import medikit
8 |
9 |
10 | @contextmanager
11 | def deprecated_feature(deprecated_in, removed_in, feature, replacement):
12 | current_version = version.parse(medikit.__version__)
13 | deprecated_in = version.parse(deprecated_in)
14 | removed_in = version.parse(removed_in)
15 | print(current_version, deprecated_in, removed_in)
16 | print(sys.warnoptions)
17 | if current_version < deprecated_in:
18 | warnings.warn(
19 | "Using {} is pending deprecation starting at {} and will stop working at {}.\nPlease use {} instead.".format(
20 | feature, deprecated_in, removed_in, replacement
21 | ),
22 | PendingDeprecationWarning,
23 | )
24 | yield
25 | elif current_version < removed_in:
26 | warnings.warn(
27 | "Using {} is deprecated since {} and will stop working at {}.\nPlease use {} instead.".format(
28 | feature, deprecated_in, removed_in, replacement
29 | ),
30 | DeprecationWarning,
31 | )
32 | yield
33 | else:
34 | raise RuntimeError(
35 | "Using {} is not supported anymore since {}. Use {} instead.".format(feature, removed_in, replacement)
36 | )
37 |
--------------------------------------------------------------------------------
/medikit/config/__init__.py:
--------------------------------------------------------------------------------
1 | import runpy
2 |
3 | import medikit
4 | from medikit.compat import deprecated_feature
5 | from medikit.config.defaults import setup_default_pipelines
6 | from medikit.config.registry import ConfigurationRegistry
7 | from medikit.settings import DEFAULT_FEATURES
8 | from medikit.utils import format_file_content
9 |
10 |
11 | def read_configuration(dispatcher, filename, variables, features, files):
12 | config = ConfigurationRegistry(dispatcher)
13 | setup_default_pipelines(config)
14 | default_context = {"listen": dispatcher.listen}
15 |
16 | # monkey patch placeholders
17 | _listen, _pipeline, _require = medikit.listen, medikit.pipeline, medikit.require
18 | try:
19 | medikit.listen, medikit.pipeline, medikit.require = dispatcher.listen, config.pipeline, config.require
20 | context = runpy.run_path(filename, init_globals=default_context)
21 | finally:
22 | # restore old values
23 | medikit.listen, medikit.pipeline, medikit.require = _listen, _pipeline, _require
24 |
25 | # Deprecated, but can be used for non-python projects (PACKAGE=...)
26 | variables = {k: context.get(k, v) for k, v in variables.items()}
27 | config.set_vars(**variables)
28 |
29 | for feature in DEFAULT_FEATURES:
30 | config.require(feature)
31 |
32 | # Deprecated: enabled and disabled features.
33 | enable_features, disable_features = context.pop("enable_features", ()), context.pop("disable_features", ())
34 | if len(enable_features) or len(disable_features):
35 | with deprecated_feature("0.5.0", "0.6.0", 'Using "enable_features" and "disable_features"', "require()"):
36 | for feature in set(enable_features) - set(disable_features):
37 | config.require(feature)
38 |
39 | # Better: required features.
40 | features = features | set(config.keys())
41 |
42 | for k in files.keys():
43 | if k in context:
44 | files[k] = format_file_content(context[k])
45 |
46 | return variables, features, files, config
47 |
--------------------------------------------------------------------------------
/medikit/config/defaults.py:
--------------------------------------------------------------------------------
1 | from medikit import steps
2 |
3 |
4 | def setup_default_pipelines(config):
5 | # todo, move this in a configurable place
6 | with config.pipeline("release") as release:
7 | release.add(steps.Install())
8 | release.add(steps.BumpVersion())
9 | release.add(steps.Make("update-requirements"))
10 | release.add(steps.Make("clean install")) # test docs
11 | release.add(steps.System("git add -p .", interractive=True))
12 | release.add(steps.Commit("Release: {version}", tag=True))
13 |
--------------------------------------------------------------------------------
/medikit/config/loader.py:
--------------------------------------------------------------------------------
1 | from stevedore import ExtensionManager
2 |
3 | _all_features = {}
4 |
5 |
6 | def load_feature_extensions():
7 | if not _all_features:
8 |
9 | def register_feature(ext, all_features=_all_features):
10 | all_features[ext.name] = ext.plugin
11 |
12 | mgr = ExtensionManager(namespace="medikit.feature")
13 | mgr.map(register_feature)
14 |
15 | return _all_features
16 |
--------------------------------------------------------------------------------
/medikit/config/registry.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import runpy
4 | from collections import OrderedDict
5 | from contextlib import contextmanager
6 |
7 | from mondrian import term
8 | from stevedore import ExtensionManager
9 | from whistle import EventDispatcher
10 |
11 | import medikit
12 | from medikit.pipeline import Pipeline
13 | from medikit.utils import run_command
14 |
15 | logger = logging.getLogger(__name__)
16 |
17 |
18 | class ConfigurationRegistry:
19 | def __init__(self, dispatcher: EventDispatcher):
20 | self._configs = {}
21 | self._features = {}
22 | self._pipelines = {}
23 | self._variables = OrderedDict()
24 |
25 | self.dispatcher = dispatcher
26 | self.resources = OrderedDict()
27 |
28 | def register_feature(ext):
29 | self._features[ext.name] = ext.plugin
30 |
31 | def on_load_feature_failure(mgr, entrypoint, err):
32 | logger.exception("Exception caught while loading {}.".format(entrypoint), err)
33 |
34 | mgr = ExtensionManager(namespace="medikit.feature", on_load_failure_callback=on_load_feature_failure)
35 | mgr.map(register_feature)
36 |
37 | dispatcher.add_listener(medikit.on_end, self.write_resources)
38 |
39 | def __getitem__(self, item):
40 | return self._configs[item]
41 |
42 | def __contains__(self, item):
43 | return item in self._configs
44 |
45 | def set_vars(self, **variables):
46 | self._variables.update(variables)
47 |
48 | def get_var(self, name, default=None):
49 | return self._variables.get(name, default)
50 |
51 | @property
52 | def variables(self):
53 | return self._variables
54 |
55 | @property
56 | def package_name(self):
57 | if "python" in self:
58 | return self["python"].get("name")
59 | else:
60 | name = self.get_var("PACKAGE")
61 | if not name:
62 | raise RuntimeError("You must define a package name, using either python.setup() or PACKAGE = ...")
63 | return name
64 |
65 | def get_name(self):
66 | return self.package_name
67 |
68 | def get_version_file(self):
69 | if "python" in self:
70 | return self["python"].version_file
71 | elif self.get_var("VERSION_FILE"):
72 | return self.get_var("VERSION_FILE")
73 | return "version.txt"
74 |
75 | def get_version(self):
76 | if "python" in self:
77 | try:
78 | return run_command("python setup.py --version")
79 | except RuntimeError:
80 | pass # ignore and fallback to alternative version getters
81 |
82 | version_file = self.get_version_file()
83 | if os.path.splitext(version_file)[1] == ".py":
84 | return runpy.run_path(version_file).get("__version__")
85 | else:
86 | with open(version_file) as f:
87 | return f.read().strip()
88 |
89 | def keys(self):
90 | return self._configs.keys()
91 |
92 | def require(self, *args):
93 | if len(args) == 1:
94 | return self._require(args[0])
95 | elif len(args) > 1:
96 | return tuple(map(self._require, args))
97 | raise ValueError("Empty.")
98 |
99 | @contextmanager
100 | def pipeline(self, name):
101 | if not name in self._pipelines:
102 | self._pipelines[name] = Pipeline()
103 | yield self._pipelines[name]
104 |
105 | @property
106 | def pipelines(self):
107 | return self._pipelines
108 |
109 | def _require(self, name):
110 | if not name in self._features:
111 | raise ValueError("Unknown feature {!r}.".format(name))
112 |
113 | if name not in self._configs:
114 | self._configs[name] = self._features[name].Config()
115 |
116 | return self._configs[name]
117 |
118 | def get_resource(self, target):
119 | if not target in self.resources:
120 | raise RuntimeError(
121 | 'Resource for "{}" is not defined, you must define it first by providing an implementation.'.format(
122 | target
123 | )
124 | )
125 | return self.resources[target]
126 |
127 | def define_resource(self, ResourceType, target, *args, **kwargs):
128 | if target in self.resources:
129 | raise RuntimeError(
130 | 'Resource for "{}" is already defined as {!r}, you can only enhance it at this point.'.format(
131 | target, self.resources[target]
132 | )
133 | )
134 | self.resources[target] = ResourceType(*args, **kwargs)
135 | return self.resources[target]
136 |
137 | def write_resources(self, event):
138 | for target, resource in self.resources.items():
139 | exists = os.path.exists(target)
140 | resource.write(event, target)
141 | self.dispatcher.info(term.bold((term.red if exists else term.green)("W!")), target)
142 |
--------------------------------------------------------------------------------
/medikit/events.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import textwrap
3 | from collections import OrderedDict
4 |
5 | from mondrian import term
6 | from whistle import Event, EventDispatcher
7 |
8 | logger = logging.getLogger()
9 |
10 |
11 | class ProjectEvent(Event):
12 | """
13 | :attr OrderedDict variables:
14 | :attr dict files:
15 | :attr OrderedDict setup:
16 | """
17 |
18 | def __init__(self, *, config, variables=None, files=None, setup=None):
19 | """
20 | :param OrderedDict|NoneType variables:
21 | """
22 | self.config = config
23 | self.variables = OrderedDict(variables or {})
24 | self.files = dict(files or {})
25 | self.setup = OrderedDict(setup or {})
26 |
27 | super(ProjectEvent, self).__init__()
28 |
29 |
30 | class LoggingDispatcher(EventDispatcher):
31 | logger = logging.getLogger()
32 | indent_level = 0
33 |
34 | @property
35 | def indent(self):
36 | return " " * type(self).indent_level
37 |
38 | def dispatch(self, event_id, event=None):
39 | should_log = not event_id.startswith("medikit.on_file_") or self.logger.getEffectiveLevel() <= logging.DEBUG
40 | if should_log:
41 | self.logger.info(
42 | self.indent
43 | + term.bold(">")
44 | + " dispatch ⚡ {} ({})".format(term.bold(term.blue(event_id)), type(event or Event).__name__)
45 | )
46 | type(self).indent_level += 1
47 | event = super(LoggingDispatcher, self).dispatch(event_id, event)
48 | type(self).indent_level -= 1
49 | if should_log:
50 | self.logger.info(self.indent + term.bold("<") + " {}".format(term.lightblack("dispatched " + event_id)))
51 | return event
52 |
53 | def debug(self, feature, *messages):
54 | return self.logger.debug(" ✔ " + term.bold(term.green(feature.__shortname__)) + " ".join(map(str, messages)))
55 |
56 | def info(self, *messages):
57 | return self.logger.info(self.indent + term.lightblack("∙") + " " + " ".join(map(str, messages)))
58 |
59 |
60 | def subscribe(event_id, priority=0):
61 | """
62 | Lazy event subscription. Will need to be attached to an event dispatcher using ``attach_subscriptions``
63 |
64 | :param str event_id:
65 | :param int priority:
66 | :return:
67 | """
68 |
69 | def wrapper(f):
70 | try:
71 | getattr(f, "__subscriptions__")
72 | except AttributeError as e:
73 | f.__subscriptions__ = {}
74 | f.__doc__ = "" if f.__doc__ is None else textwrap.dedent(f.__doc__).strip()
75 | if f.__doc__:
76 | f.__doc__ += "\n"
77 |
78 | f.__subscriptions__[event_id] = priority
79 | f.__doc__ += "\nListens to ``{}`` event *(priority: {})*".format(event_id, priority)
80 |
81 | return f
82 |
83 | return wrapper
84 |
85 |
86 | def attach_subscriptions(obj, dispatcher):
87 | """
88 | Attach subscriptions to an actual event dispatcher.
89 |
90 | :param object obj:
91 | :param EventDispatcher dispatcher:
92 | :return:
93 | """
94 | for k in dir(obj):
95 | f = getattr(obj, k)
96 | if k[0] != "_" and callable(f) and hasattr(f, "__subscriptions__"):
97 | for event_id, priority in f.__subscriptions__.items():
98 | dispatcher.add_listener(event_id, f, priority=priority)
99 |
--------------------------------------------------------------------------------
/medikit/feature/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from collections import OrderedDict
4 | from contextlib import ContextDecorator
5 |
6 | from jinja2 import Environment, PackageLoader, Template
7 | from mondrian import term
8 | from yapf import yapf_api
9 |
10 | from medikit import settings
11 | from medikit.events import attach_subscriptions
12 | from medikit.file import File
13 | from medikit.settings import DEFAULT_FEATURES
14 | from medikit.utils import format_file_content, is_identifier
15 |
16 | ABSOLUTE_PRIORITY = -100
17 | HIGH_PRIORITY = -80
18 | MEDIUM_PRIORITY = -60
19 | LOW_PRIORITY = -60
20 | SUPPORT_PRIORITY = -20
21 | LAST_PRIORITY = 100
22 |
23 |
24 | class Feature(object):
25 | _jinja_environment = None
26 |
27 | requires = set()
28 | conflicts = set()
29 |
30 | file_type = staticmethod(File)
31 |
32 | __usage__ = None
33 |
34 | class Config(ContextDecorator):
35 | def __enter__(self):
36 | return self
37 |
38 | def __exit__(self, *exc):
39 | return False
40 |
41 | def __init__(self, dispatcher):
42 | """
43 |
44 | :param LoggingDispatcher dispatcher:
45 | """
46 | self.dispatcher = dispatcher
47 | self.configure()
48 | attach_subscriptions(self, self.dispatcher)
49 |
50 | def configure(self):
51 | pass
52 |
53 | def file(self, *args, **kwargs):
54 | return self.file_type(self.dispatcher, *args, **kwargs)
55 |
56 | @property
57 | def __name__(self):
58 | return type(self).__name__
59 |
60 | @property
61 | def __shortname__(self):
62 | return self.__name__.replace("Feature", "").lower()
63 |
64 | @property
65 | def jinja(self):
66 | if type(self)._jinja_environment is None:
67 | type(self)._jinja_environment = Environment(loader=PackageLoader(__name__, "template"))
68 | return type(self)._jinja_environment
69 |
70 | def _log_file(self, target, override, content=()):
71 | self.dispatcher.info(
72 | term.bold(term.red("W!") if override else term.green("W?")), target, "({} bytes)".format(len(content))
73 | )
74 |
75 | def render(self, template, context=None):
76 | context = context or {}
77 | os.path.join(os.path.dirname(__file__), "template")
78 | return self.jinja.get_template(template).render(**(context or {}))
79 |
80 | def render_file(self, target, template, context=None, *, executable=False, override=False, force_python=False):
81 | with self.file(target, executable=executable, override=override) as f:
82 | content = format_file_content(self.render(template, context))
83 | if force_python or target.endswith(".py"):
84 | content, modified = yapf_api.FormatCode(content, filename=target)
85 | f.write(content)
86 | self._log_file(target, override, content)
87 |
88 | def render_file_inline(self, target, template_string, context=None, override=False, force_python=False):
89 | with self.file(target, override=override) as f:
90 | content = format_file_content(Template(template_string).render(**(context or {})))
91 | if force_python or target.endswith(".py"):
92 | content, modified = yapf_api.FormatCode(
93 | content, filename=target, style_config=settings.YAPF_STYLE_CONFIG
94 | )
95 | f.write(content)
96 | self._log_file(target, override, content)
97 |
98 | def render_empty_files(self, *targets, **kwargs):
99 | override = kwargs.pop("override", False)
100 | for target in targets:
101 | with self.file(target, override=override) as f:
102 | self._log_file(target, override)
103 |
104 | def get_config(self, event, feature=None):
105 | """
106 | Retrieve the config object for a feature, defaults to current feature.
107 |
108 | :param event: event from which to extract the config.
109 | :param feature: feature name, or None for current.
110 | :return: Feature.Config
111 | """
112 | return event.config[feature or self.__shortname__]
113 |
114 |
115 | class ProjectInitializer(Feature):
116 | def __init__(self, dispatcher, options):
117 | super().__init__(dispatcher)
118 | self.options = options
119 |
120 | def execute(self):
121 | context = {}
122 |
123 | if self.options.get("name"):
124 | if not is_identifier(self.options["name"]):
125 | raise RuntimeError("Invalid package name {!r}.".format(self.options["name"]))
126 | context["name"] = self.options["name"]
127 | logging.info("name = %s", context["name"])
128 | else:
129 | context["name"] = input("Name: ")
130 | while not is_identifier(context["name"]):
131 | logging.error("Invalid name. Please only use valid python identifiers.")
132 | context["name"] = input("Name: ")
133 |
134 | if self.options.get("description"):
135 | context["description"] = self.options["description"]
136 | else:
137 | context["description"] = input("Description: ")
138 |
139 | if self.options.get("license"):
140 | context["license"] = self.options["license"]
141 | else:
142 | context["license"] = (
143 | input("License [Apache License, Version 2.0]: ").strip() or "Apache License, Version 2.0"
144 | )
145 |
146 | context["url"] = ""
147 | context["download_url"] = ""
148 | context["author"] = ""
149 | context["author_email"] = ""
150 |
151 | context["features"] = DEFAULT_FEATURES
152 | if self.options.get("features"):
153 | context["features"] = context["features"].union(self.options["features"])
154 |
155 | context["requirements"] = self.options.get("requirements", [])
156 |
157 | self.render_file("Projectfile", "Projectfile.j2", context, override=True, force_python=True)
158 |
--------------------------------------------------------------------------------
/medikit/feature/django.py:
--------------------------------------------------------------------------------
1 | """
2 | The «django» feature adds the django framework to your project.
3 |
4 | """
5 |
6 | import os
7 | import random
8 |
9 | import medikit
10 | from medikit.events import subscribe
11 |
12 | from . import SUPPORT_PRIORITY, Feature
13 |
14 | random = random.SystemRandom()
15 |
16 |
17 | def generate_secret_key():
18 | chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"
19 | return "".join(random.choice(chars) for i in range(64))
20 |
21 |
22 | class DjangoConfig(Feature.Config):
23 | """ Configuration class for the “django” feature. """
24 |
25 | version = "~=2.0,<2.2"
26 | """Which django version requirement do you want?"""
27 |
28 | __usage__ = """
29 |
30 | This will add a few items to your Makefile, and ensure a minimalistic django project structure is available.
31 |
32 | By default, it will use Django {version}, but you can tune that:
33 |
34 | .. code-block:: python
35 |
36 | django.version = '~=2.0.3'
37 |
38 | This feature will also add or extend a "prod" python extra, that will install gunicorn.
39 |
40 | """.format(
41 | version=version
42 | )
43 |
44 | def __init__(self):
45 | self._static_dir = None
46 | self.version = self.version
47 |
48 | @property
49 | def static_dir(self):
50 | """
51 | The django global static directory (property).
52 | """
53 | return self._static_dir
54 |
55 | @static_dir.setter
56 | def static_dir(self, value):
57 | self._static_dir = value
58 |
59 |
60 | class DjangoFeature(Feature):
61 | requires = {"python"}
62 |
63 | Config = DjangoConfig
64 |
65 | @subscribe("medikit.feature.python.on_generate")
66 | def on_python_generate(self, event):
67 | event.config["python"].add_requirements("django " + event.config["django"].version, prod=["gunicorn ==19.7.1"])
68 |
69 | @subscribe("medikit.feature.make.on_generate", priority=SUPPORT_PRIORITY)
70 | def on_make_generate(self, event):
71 | makefile = event.makefile
72 | makefile["DJANGO"] = "$(PYTHON) manage.py"
73 | makefile.add_target("runserver", """$(DJANGO) runserver""", deps=("install-dev",), phony=True)
74 |
75 | @subscribe(medikit.on_start)
76 | def on_start(self, event):
77 | django_config = event.config["django"]
78 | python_config = event.config["python"]
79 |
80 | context = {**python_config.get_setup(), "config_package": "config", "secret_key": generate_secret_key()}
81 |
82 | # Create configuration
83 | config_path = "config"
84 | if not os.path.exists(config_path):
85 | os.makedirs(config_path)
86 |
87 | self.render_file("manage.py", "django/manage.py.j2", context, executable=True, force_python=True)
88 | self.render_file(os.path.join(config_path, "settings.py"), "django/settings.py.j2", context, force_python=True)
89 | self.render_file(os.path.join(config_path, "urls.py"), "django/urls.py.j2", context, force_python=True)
90 | self.render_file(os.path.join(config_path, "wsgi.py"), "django/wsgi.py.j2", context, force_python=True)
91 |
92 | self.dispatcher.dispatch("medikit.feature.django.on_configure")
93 |
94 | static_dir = django_config.static_dir or os.path.join(python_config.package_dir, "static")
95 | if not os.path.exists(static_dir):
96 | os.makedirs(static_dir)
97 | self.render_empty_files(os.path.join(static_dir, "favicon.ico"))
98 |
--------------------------------------------------------------------------------
/medikit/feature/format.py:
--------------------------------------------------------------------------------
1 | """
2 | Code formating, using third party tools.
3 |
4 | Superseeds "yapf" feature that was only using one tool for one language.
5 |
6 | .. code-block:: shell-session
7 |
8 | $ make format
9 |
10 | """
11 | import functools
12 | import os
13 |
14 | import medikit
15 | from medikit import settings
16 | from medikit.events import subscribe
17 | from medikit.feature import ABSOLUTE_PRIORITY, SUPPORT_PRIORITY, Feature
18 | from medikit.feature.make import which
19 | from medikit.globals import LINE_LENGTH
20 | from medikit.structs import Script
21 |
22 |
23 | class FormatConfig(Feature.Config):
24 | python_tools = {"yapf", "black", "isort"}
25 | javascript_tools = {"prettier"}
26 | default_tools = {"black", "isort"}
27 | all_tools = functools.reduce(set.union, [javascript_tools, python_tools], set())
28 | _active_tools = set()
29 |
30 | def using(self, *tools):
31 | """
32 | Choose which tool to use when formatting the codebase.
33 |
34 | Example::
35 |
36 | require("format").using("yapf", "isort")
37 |
38 | Note that the above is also the default, so using `require("format")` is enough to have the same effect.
39 |
40 | """
41 | for tool in tools:
42 | if tool not in self.all_tools:
43 | raise ValueError('Unknown formating tool "{}".'.format(tool))
44 | self._active_tools.add(tool)
45 |
46 | @property
47 | def active_tools(self):
48 | return self._active_tools or self.default_tools
49 |
50 |
51 | class FormatFeature(Feature):
52 | Config = FormatConfig
53 |
54 | conflicts = {"yapf"}
55 |
56 | @subscribe("medikit.feature.python.on_generate")
57 | def on_python_generate(self, event):
58 | config = self.get_config(event) # type: FormatConfig
59 | python = self.get_config(event, "python")
60 | for tool in FormatConfig.python_tools:
61 | if tool == "black":
62 | # Black is only available starting with python 3.6, it will be up to the user to install it.
63 | continue
64 | if tool in config.active_tools:
65 | python.add_requirements(dev=[tool])
66 |
67 | @subscribe("medikit.feature.make.on_generate", priority=SUPPORT_PRIORITY)
68 | def on_make_generate(self, event):
69 | config = self.get_config(event) # type: FormatConfig
70 | makefile = event.makefile
71 |
72 | if "yapf" in config.active_tools and "black" in config.active_tools:
73 | raise RuntimeError('Using both "black" and "yapf" does not make sense, choose one.')
74 |
75 | format_script = []
76 |
77 | if "black" in config.active_tools:
78 | makefile["BLACK"] = which("black")
79 | makefile["BLACK_OPTIONS"] = "--line-length " + str(LINE_LENGTH) # TODO line length global option
80 | format_script += ["$(BLACK) $(BLACK_OPTIONS) . Projectfile"]
81 |
82 | if "yapf" in config.active_tools:
83 | makefile["YAPF"] = "$(PYTHON) -m yapf"
84 | makefile["YAPF_OPTIONS"] = "-rip"
85 | format_script += ["$(YAPF) $(YAPF_OPTIONS) . Projectfile"]
86 |
87 | if "isort" in config.active_tools:
88 | makefile["ISORT"] = "$(PYTHON) -m isort"
89 | makefile["ISORT_OPTIONS"] = ""
90 | format_script += ["$(ISORT) $(ISORT_OPTIONS) . Projectfile"]
91 |
92 | if "prettier" in config.active_tools:
93 | makefile["PRETTIER"] = which("prettier")
94 | makefile["PRETTIER_OPTIONS"] = "--write"
95 | makefile["PRETTIER_PATTERNS"] = "**/*.\{j,t\}s **/*.\{j,t\}sx \!docs/**"
96 | format_script += ["$(PRETTIER) $(PRETTIER_OPTIONS) $(PRETTIER_PATTERNS)"]
97 |
98 | makefile.add_target(
99 | "format",
100 | Script("\n".join(format_script)),
101 | deps=("install-dev",),
102 | phony=True,
103 | doc="Reformats the codebase (with " + ", ".join(sorted(config.active_tools)) + ").",
104 | )
105 |
106 | @subscribe(medikit.on_start, priority=SUPPORT_PRIORITY)
107 | def on_start(self, event):
108 | config = self.get_config(event) # type: FormatConfig
109 | if "isort" in config.active_tools:
110 | with event.config.get_resource("setup.cfg") as setup_cfg:
111 | setup_cfg.set_managed_values({"isort": {"line_length": str(LINE_LENGTH)}})
112 | if "yapf" in config.active_tools:
113 | self.render_file(".style.yapf", "yapf/style.yapf.j2")
114 |
115 | @subscribe(medikit.on_start, priority=ABSOLUTE_PRIORITY - 1)
116 | def on_before_start(self, event):
117 | config = self.get_config(event) # type: FormatConfig
118 | if "yapf" in config.active_tools:
119 | style_config = os.path.join(os.getcwd(), ".style.yapf")
120 | if os.path.exists(style_config):
121 | self.dispatcher.info("YAPF_STYLE_CONFIG = " + style_config)
122 | settings.YAPF_STYLE_CONFIG = style_config
123 |
--------------------------------------------------------------------------------
/medikit/feature/git.py:
--------------------------------------------------------------------------------
1 | """
2 | Git version control system support.
3 |
4 | """
5 |
6 | import os
7 |
8 | import medikit
9 | from medikit.events import subscribe
10 | from medikit.feature import ABSOLUTE_PRIORITY, Feature
11 |
12 |
13 | class GitConfig(Feature.Config):
14 | def __init__(self):
15 | self._enabled = True
16 |
17 | @property
18 | def enabled(self):
19 | return self._enabled
20 |
21 | def disable(self):
22 | self._enabled = False
23 |
24 | def enable(self):
25 | self._enabled = True
26 |
27 |
28 | class GitFeature(Feature):
29 | Config = GitConfig
30 |
31 | @subscribe(medikit.on_start, priority=ABSOLUTE_PRIORITY)
32 | def on_start(self, event):
33 | if not event.config["git"].enabled:
34 | return
35 |
36 | if not os.path.exists(".git"):
37 | self.dispatcher.info("git", "Creating git repository...")
38 | os.system("git init --quiet")
39 | os.system("git add Projectfile")
40 | os.system('git commit --quiet -m "Project initialized using Medikit."')
41 |
42 | def on_file_change(event):
43 | os.system("git add {}".format(event.filename))
44 |
45 | self.dispatcher.add_listener("medikit.on_file_closed", on_file_change, priority=-1)
46 |
47 | @subscribe(medikit.on_end)
48 | def on_end(self, event):
49 | self.render_file_inline(
50 | ".gitignore",
51 | """
52 | *.egg-info
53 | *.iml
54 | *.pyc
55 | *.swp
56 | /.cache
57 | /.coverage
58 | /.idea
59 | /.python*-*
60 | /build
61 | /dist
62 | /htmlcov
63 | /pylint.html
64 | """,
65 | event.variables,
66 | )
67 |
68 | @subscribe("medikit.feature.make.on_generate", priority=ABSOLUTE_PRIORITY + 1)
69 | def on_make_generate(self, event):
70 | event.makefile["VERSION"] = "$(shell git describe 2>/dev/null || git rev-parse --short HEAD)"
71 |
--------------------------------------------------------------------------------
/medikit/feature/kube.py:
--------------------------------------------------------------------------------
1 | """
2 | Setup make targets to rollout and rollback this project as a deployment onto a Kubernetes cluster.
3 |
4 | """
5 | import json
6 |
7 | from medikit.events import subscribe
8 | from medikit.feature import Feature
9 | from medikit.feature.make import which
10 |
11 |
12 | class KubeConfig(Feature.Config):
13 | def __init__(self):
14 | self._targets = dict()
15 | self._targets_patches = dict()
16 | self._use_helm = False
17 |
18 | def add_target(self, name, variant=None, *, patch, patch_path=""):
19 | if not variant in self._targets:
20 | self._targets[variant] = list()
21 | self._targets_patches[variant] = dict()
22 |
23 | if name in self._targets[variant]:
24 | raise ValueError("Kubernetes target {} already defined.".format(name))
25 |
26 | self._targets[variant].append(name)
27 | self._targets_patches[variant][name] = patch_path, patch
28 |
29 | def get_variants(self):
30 | return list(self._targets.keys())
31 |
32 | def get_targets(self, variant=None):
33 | for target in self._targets[variant]:
34 | yield target, self._targets_patches[variant][target]
35 |
36 | @property
37 | def use_helm(self):
38 | return self._use_helm
39 |
40 | def enable_helm(self):
41 | self._use_helm = True
42 | return self
43 |
44 | def disable_helm(self):
45 | self._use_helm = False
46 | return self
47 |
48 |
49 | class KubeFeature(Feature):
50 | Config = KubeConfig
51 |
52 | requires = {"docker"}
53 |
54 | @subscribe("medikit.feature.make.on_generate", priority=-1)
55 | def on_make_generate(self, event):
56 | kube_config = event.config["kube"]
57 |
58 | event.makefile["KUBECTL"] = which("kubectl")
59 | event.makefile["KUBECTL_OPTIONS"] = ""
60 | event.makefile["KUBECONFIG"] = ""
61 | event.makefile["KUBE_NAMESPACE"] = "default"
62 |
63 | if kube_config.use_helm:
64 | event.makefile["HELM"] = which("helm")
65 | event.makefile["HELM_RELEASE"] = event.config.get_name()
66 |
67 | for variant in kube_config.get_variants():
68 | targets = list(kube_config.get_targets(variant=variant))
69 | if len(targets):
70 | rollout_target = "-".join(filter(None, ("kube-rollout", variant)))
71 | rollback_target = "-".join(filter(None, ("kube-rollback", variant)))
72 |
73 | rollout_commands, rollback_commands = [], []
74 | for target, (patch_path, patch) in targets:
75 | while patch_path:
76 | try:
77 | patch_path, _bit = patch_path.rsplit(".", 1)
78 | except ValueError:
79 | patch_path, _bit = None, patch_path
80 | patch = {_bit: patch}
81 |
82 | rollout_commands.append(
83 | "$(KUBECTL) $(KUBECTL_OPTIONS) --namespace=$(KUBE_NAMESPACE) patch {target} -p{patch}".format(
84 | target=target, patch=repr(json.dumps(patch))
85 | )
86 | )
87 |
88 | rollback_commands.append("$(KUBECTL) rollout undo {target}".format(target=target))
89 |
90 | event.makefile.add_target(
91 | rollout_target,
92 | "\n".join(rollout_commands),
93 | phony=True,
94 | doc="Rollout docker image onto kubernetes cluster.",
95 | )
96 |
97 | event.makefile.add_target(
98 | rollback_target,
99 | "\n".join(rollback_commands),
100 | phony=True,
101 | doc="Rollbacks last kubernetes patch operation.",
102 | )
103 |
--------------------------------------------------------------------------------
/medikit/feature/make/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | GNU Make support.
3 |
4 | """
5 |
6 | import medikit
7 | from medikit.events import subscribe
8 | from medikit.feature import HIGH_PRIORITY, Feature
9 | from medikit.feature.make.config import MakeConfig
10 | from medikit.feature.make.events import MakefileEvent
11 | from medikit.feature.make.resources import CleanScript, InstallScript, Makefile, MakefileTarget
12 | from medikit.feature.make.utils import which
13 | from medikit.globals import PIP_VERSION
14 | from medikit.structs import Script
15 |
16 | __all__ = [
17 | "CleanScript",
18 | "InstallScript",
19 | "MakeConfig",
20 | "MakeFeature",
21 | "Makefile",
22 | "MakefileEvent",
23 | "MakefileTarget",
24 | "which",
25 | ]
26 |
27 |
28 | class MakeFeature(Feature):
29 | Config = MakeConfig
30 |
31 | def configure(self):
32 | self.makefile = Makefile()
33 |
34 | @subscribe(medikit.on_start, priority=HIGH_PRIORITY)
35 | def on_start(self, event):
36 | """
37 | :param ProjectEvent event:
38 | """
39 |
40 | for k in event.variables:
41 | self.makefile[k.upper()] = event.variables[k]
42 |
43 | self.makefile.updateleft(("QUICK", ""))
44 |
45 | self.makefile.add_install_target()
46 |
47 | for extra in event.config["make"].extras:
48 | self.makefile.add_install_target(extra)
49 |
50 | self.makefile.add_target("quick", Script('@printf ""'), phony=True, hidden=True)
51 |
52 | self.dispatcher.dispatch(
53 | MakeConfig.on_generate, MakefileEvent(event.config.package_name, self.makefile, event.config)
54 | )
55 |
56 | if event.config["make"].include_medikit_targets:
57 | self.add_medikit_targets(event.config["make"])
58 |
59 | if event.config["make"].includes:
60 | self.add_includes(event.config["make"].includes)
61 |
62 | # Recipe courtesy of https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
63 | self.makefile.add_target(
64 | "help",
65 | r"""
66 | @echo "Available commands:"
67 | @echo
68 | @grep -E '^[a-zA-Z_-]+:.*?##[\s]?.*$$' --no-filename $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?##"}; {printf " make \033[36m%-30s\033[0m %s\n", $$1, $$2}'
69 | @echo
70 | """,
71 | phony=True,
72 | doc="Shows available commands.",
73 | )
74 |
75 | # Actual rendering of the Makefile
76 | self.render_file_inline("Makefile", self.makefile.__str__(), override=True)
77 |
78 | def add_medikit_targets(self, config):
79 | if not "PYTHON" in self.makefile:
80 | self.makefile["PYTHON"] = which("python")
81 | self.makefile["MEDIKIT"] = "$(PYTHON) -m medikit"
82 | self.makefile["MEDIKIT_UPDATE_OPTIONS"] = ""
83 | self.makefile["MEDIKIT_VERSION"] = medikit.__version__
84 |
85 | source = [
86 | "import medikit, pip, sys",
87 | "from packaging.version import Version",
88 | 'sys.exit(0 if (Version(medikit.__version__) >= Version("$(MEDIKIT_VERSION)")) and (Version(pip.__version__) < Version("10")) else 1)',
89 | ]
90 |
91 | self.makefile.add_target(
92 | "medikit",
93 | '@$(PYTHON) -c {!r} || $(PYTHON) -m pip install -U "pip {PIP_VERSION}" "medikit>=$(MEDIKIT_VERSION)"'.format(
94 | "; ".join(source), PIP_VERSION=PIP_VERSION
95 | ),
96 | phony=True,
97 | hidden=True,
98 | doc="Checks installed medikit version and updates it if it is outdated.",
99 | )
100 |
101 | self.makefile.add_target(
102 | "update",
103 | "$(MEDIKIT) update $(MEDIKIT_UPDATE_OPTIONS)",
104 | deps=("medikit",),
105 | phony=True,
106 | doc="""Update project artifacts using medikit.""",
107 | )
108 |
109 | # TODO this should adapt to langauges included, and be removed if no language
110 | # For example, requirements*.txt are specific to python, using classic setuptools.
111 | self.makefile.add_target(
112 | "update-requirements",
113 | 'MEDIKIT_UPDATE_OPTIONS="--override-requirements" $(MAKE) update',
114 | phony=True,
115 | doc="""Update project artifacts using medikit, including requirements files.""",
116 | )
117 |
118 | def add_includes(self, includes):
119 | for include in includes:
120 | self.makefile.header.append("include " + include)
121 |
--------------------------------------------------------------------------------
/medikit/feature/make/config.py:
--------------------------------------------------------------------------------
1 | from medikit.feature import Feature
2 |
3 |
4 | class MakeConfig(Feature.Config):
5 | on_generate = "medikit.feature.make.on_generate"
6 | """Happens during the makefile generation."""
7 |
8 | def __init__(self):
9 | self.include_medikit_targets = True
10 | self.extras = {"dev"}
11 | self.includes = []
12 |
13 | def disable_medikit_targets(self):
14 | self.include_medikit_targets = False
15 |
--------------------------------------------------------------------------------
/medikit/feature/make/events.py:
--------------------------------------------------------------------------------
1 | from whistle import Event
2 |
3 |
4 | class MakefileEvent(Event):
5 | def __init__(self, package_name, makefile, config):
6 | self.package_name = package_name
7 | self.makefile = makefile
8 | self.config = config
9 | super(MakefileEvent, self).__init__()
10 |
--------------------------------------------------------------------------------
/medikit/feature/make/resources.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import textwrap
3 | from collections import deque, namedtuple
4 |
5 | from medikit.structs import Script
6 | from medikit.utils import get_override_warning_banner
7 |
8 | MakefileTarget = namedtuple("MakefileTarget", ["deps", "rule", "doc"])
9 |
10 |
11 | class Makefile(object):
12 | @property
13 | def targets(self):
14 | for key in self._target_order:
15 | yield key, self._target_values[key]
16 |
17 | @property
18 | def environ(self):
19 | return self._env_values
20 |
21 | def __init__(self):
22 | self._env_order, self._env_values, self._env_assignment_operators = deque(), {}, {}
23 | self._target_order, self._target_values = deque(), {}
24 | self.hidden = set()
25 | self.phony = set()
26 | self.header = []
27 |
28 | def __delitem__(self, key):
29 | self._env_order.remove(key)
30 | del self._env_values[key]
31 |
32 | def __contains__(self, item):
33 | return item in self._env_values
34 |
35 | def __getitem__(self, item):
36 | return self._env_values[item]
37 |
38 | def __setitem__(self, key, value):
39 | self._env_values[key] = value
40 | if not key in self._env_order:
41 | self._env_order.append(key)
42 |
43 | def __iter__(self):
44 | for key in self._env_order:
45 | yield key, self._env_values[key]
46 |
47 | def __len__(self):
48 | return len(self._env_order)
49 |
50 | def __str__(self):
51 | content = [get_override_warning_banner(), ""] + self.header + [""]
52 |
53 | if len(self):
54 | for k, v in self:
55 | v = textwrap.dedent(str(v)).strip()
56 | v = v.replace("\n", " \\\n" + " " * (len(k) + 4))
57 | content.append("{} {} {}".format(k, self._env_assignment_operators.get(k, "?="), v))
58 | content.append("")
59 |
60 | if len(self.phony):
61 | content.append(".PHONY: " + " ".join(sorted(self.phony)))
62 | content.append("")
63 |
64 | for target, details in self.targets:
65 | deps, rule, doc = details
66 |
67 | if hasattr(rule, "render"):
68 | content += list(rule.render(target, deps, doc))
69 | else:
70 | content.append(
71 | "{}: {} {} {}".format(
72 | target,
73 | " ".join(deps),
74 | "#" if target in self.hidden else "##",
75 | doc.replace("\n", " ") if doc else "",
76 | ).strip()
77 | )
78 |
79 | script = textwrap.dedent(str(rule)).strip()
80 |
81 | for line in script.split("\n"):
82 | content.append("\t" + line)
83 |
84 | content.append("")
85 |
86 | return "\n".join(content)
87 |
88 | def keys(self):
89 | return list(self._env_order)
90 |
91 | def add_target(self, target, rule, *, deps=None, phony=False, first=False, doc=None, hidden=False):
92 | if target in self._target_order:
93 | raise RuntimeError("Duplicate definition for make target «{}».".format(target))
94 |
95 | if isinstance(rule, str):
96 | rule = Script(rule)
97 |
98 | self._target_values[target] = MakefileTarget(
99 | deps=tuple(deps) if deps else tuple(), rule=rule, doc=textwrap.dedent(doc or "").strip()
100 | )
101 |
102 | self._target_order.appendleft(target) if first else self._target_order.append(target)
103 |
104 | if phony:
105 | self.phony.add(target)
106 |
107 | if hidden:
108 | self.hidden.add(target)
109 |
110 | def get_clean_target(self):
111 | if not self.has_target("clean"):
112 | self.add_target("clean", CleanScript(), phony=True, doc="""Cleans up the working copy.""")
113 | return self.get_target("clean")
114 |
115 | def add_install_target(self, extra=None):
116 | if extra:
117 | target = "install-" + extra
118 | doc = "Installs the project (with " + extra + " dependencies)."
119 | else:
120 | target = "install"
121 | doc = "Installs the project."
122 |
123 | if not self.has_target(target):
124 | self.add_target(target, InstallScript(), phony=True, doc=doc)
125 |
126 | clean_target = self.get_clean_target()
127 | marker = ".medikit/" + target
128 | if not marker in clean_target.remove:
129 | clean_target.remove.append(marker)
130 | return self.get_target(target)
131 |
132 | def get_target(self, target):
133 | return self._target_values[target][1]
134 |
135 | def has_target(self, target):
136 | return target in self._target_values
137 |
138 | def set_deps(self, target, deps=None):
139 | self._target_values[target] = (deps or list(), self._target_values[target][1], self._target_values[target][2])
140 | return self
141 |
142 | def set_script(self, target, script):
143 | self._target_values[target] = (self._target_values[target][0], script, self._target_values[target][2])
144 | return self
145 |
146 | def set_assignment_operator(self, key, value):
147 | assert value in ("?=", "=", "+=", ":=", "::=", "!="), "Invalid operator"
148 | self._env_assignment_operators[key] = value
149 |
150 | def setleft(self, key, value):
151 | self._env_values[key] = value
152 | if key in self._env_order:
153 | self._env_order.remove(key)
154 | self._env_order.appendleft(key)
155 |
156 | def updateleft(self, *lst):
157 | for key, value in reversed(lst):
158 | self.setleft(key, value)
159 |
160 |
161 | class InstallScript(Script):
162 | def __init__(self, script=None):
163 | super(InstallScript, self).__init__(script)
164 |
165 | self.before_install = []
166 | self.install = self.script
167 | self.after_install = []
168 |
169 | self.deps = []
170 |
171 | def __iter__(self):
172 | yield '@if [ -z "$(QUICK)" ]; then \\'
173 | for line in map(
174 | lambda x: " {} ; \\".format(x), itertools.chain(self.before_install, self.install, self.after_install)
175 | ):
176 | yield line
177 | yield "fi"
178 |
179 | def render(self, name, deps, doc):
180 | tab = "\t"
181 | yield "{name}: .medikit/{name} {deps} ## {doc}".format(name=name, deps=" ".join(deps), doc=doc)
182 | yield ".medikit/{name}: {deps}".format(name=name, deps=" ".join(sorted(set(self.deps))))
183 | yield tab + "$(eval target := $(shell echo $@ | rev | cut -d/ -f1 | rev))"
184 | yield "ifeq ($(filter quick,$(MAKECMDGOALS)),quick)"
185 | yield tab + r'@printf "Skipping \033[36m%s\033[0m because of \033[36mquick\033[0m target.\n" $(target)'
186 | yield "else ifneq ($(QUICK),)"
187 | yield tab + r'@printf "Skipping \033[36m%s\033[0m because \033[36m$$QUICK\033[0m is not empty.\n" $(target)'
188 | yield "else"
189 | yield tab + r'@printf "Applying \033[36m%s\033[0m target...\n" $(target)'
190 | for line in itertools.chain(self.before_install, self.install, self.after_install):
191 | yield tab + line
192 | yield tab + "@mkdir -p .medikit; touch $@"
193 | yield "endif"
194 |
195 |
196 | class CleanScript(Script):
197 | # We should not clean .medikit subdirectories here, as it will deny releases.
198 | # TODO: move into python feature
199 | remove = ["build", "dist", "*.egg-info"]
200 |
201 | def __iter__(self):
202 | # cleanup build directories
203 | yield "rm -rf {}".format(" ".join(self.remove))
204 | # cleanup python bytecode -
205 | yield "find . -name __pycache__ -type d | xargs rm -rf"
206 |
--------------------------------------------------------------------------------
/medikit/feature/make/utils.py:
--------------------------------------------------------------------------------
1 | from medikit import which
2 |
3 | # only for backward compat
4 | __all__ = ["which"]
5 |
--------------------------------------------------------------------------------
/medikit/feature/nodejs.py:
--------------------------------------------------------------------------------
1 | """
2 | NodeJS / Yarn support.
3 |
4 | This feature is experimental and as though it may work for you, that's not a guarantee. Please use with care.
5 |
6 | """
7 |
8 | import json
9 | import os
10 | import runpy
11 |
12 | import medikit
13 | from medikit.events import subscribe
14 | from medikit.feature import LAST_PRIORITY, Feature
15 | from medikit.feature.make import which
16 | from medikit.steps.version import PythonVersion
17 |
18 |
19 | class NodeJSConfig(Feature.Config):
20 | """ Configuration API for the «nodejs» feature. """
21 |
22 | def __init__(self):
23 | self._setup = {}
24 | self._dependencies = {None: {}, "dev": {}, "peer": {}, "bundled": {}, "optional": {}}
25 | self.base_dir = None
26 |
27 | def setup(self, *, base_dir=None):
28 | if base_dir:
29 | self.base_dir = base_dir
30 | return self
31 |
32 | def add_dependencies(self, deps=None, **kwargs):
33 | if deps:
34 | self.__add_dependencies(deps)
35 | for deptype, deps in kwargs.items():
36 | self.__add_dependencies(deps, deptype=deptype)
37 | return self
38 |
39 | def get_dependencies(self):
40 | return {
41 | deptype + "Dependencies" if deptype else "dependencies": deps
42 | for deptype, deps in self._dependencies.items()
43 | if len(deps)
44 | }
45 |
46 | def __add_dependencies(self, deps, deptype=None):
47 | if len(deps):
48 | if deptype not in self._dependencies:
49 | raise KeyError("Invalid dependency type " + deptype)
50 | self._dependencies[deptype].update(deps)
51 |
52 |
53 | class NodeJSFeature(Feature):
54 | requires = {"python", "make"}
55 |
56 | Config = NodeJSConfig
57 |
58 | @subscribe("medikit.feature.make.on_generate")
59 | def on_make_generate(self, event):
60 | event.makefile["YARN"] = which("yarn")
61 | event.makefile["NODE"] = which("node")
62 |
63 | event.makefile.get_target("install").install += ["$(YARN) install --production"]
64 |
65 | event.makefile.get_target("install-dev").install += ["$(YARN) install"]
66 |
67 | @subscribe(medikit.on_start)
68 | def on_start(self, event):
69 | name = event.config["python"].get("name")
70 |
71 | current_version = PythonVersion.coerce(
72 | runpy.run_path(event.config["python"].version_file).get("__version__"), partial=True
73 | )
74 | current_version.partial = False
75 |
76 | package = {
77 | "name": name,
78 | "version": str(current_version),
79 | "description": event.config["python"].get("description"),
80 | "author": event.config["python"].get("author"),
81 | "license": event.config["python"].get("license"),
82 | **event.config["nodejs"].get_dependencies(),
83 | }
84 |
85 | base_dir = event.config["nodejs"].base_dir or "."
86 |
87 | self.render_file_inline(
88 | os.path.join(base_dir, "package.json"), json.dumps(package, sort_keys=True, indent=4), override=True
89 | )
90 |
91 | @subscribe(medikit.on_end, priority=LAST_PRIORITY)
92 | def on_end(self, event):
93 | base_dir = event.config["nodejs"].base_dir or "."
94 | os.system("cd {base_dir}; yarn install".format(base_dir=base_dir))
95 | os.system("cd {base_dir}; git add yarn.lock".format(base_dir=base_dir))
96 |
--------------------------------------------------------------------------------
/medikit/feature/pylint.py:
--------------------------------------------------------------------------------
1 | """
2 | Lint your python code with pylint.
3 |
4 | This feature may be outdated.
5 |
6 | """
7 | from medikit.events import subscribe
8 |
9 | from . import SUPPORT_PRIORITY, Feature
10 |
11 |
12 | class PylintFeature(Feature):
13 | requires = {"python"}
14 |
15 | @subscribe("medikit.feature.python.on_generate")
16 | def on_python_generate(self, event):
17 | event.config["python"].add_requirements(dev=["pylint ~=1.8"])
18 |
19 | @subscribe("medikit.feature.make.on_generate", priority=SUPPORT_PRIORITY)
20 | def on_make_generate(self, event):
21 | makefile = event.makefile
22 | makefile.add_target(
23 | "lint",
24 | """
25 | $(PYTHON_DIRNAME)/pylint --py3k $(PACKAGE) -f html > pylint.html
26 | """.format(
27 | name=event.package_name
28 | ),
29 | deps=("install-dev",),
30 | phony=True,
31 | )
32 |
--------------------------------------------------------------------------------
/medikit/feature/pytest.py:
--------------------------------------------------------------------------------
1 | """
2 | Adds the pytest testing framework to your project.
3 |
4 | """
5 |
6 | import os
7 |
8 | import medikit
9 | from medikit.events import subscribe
10 |
11 | from . import SUPPORT_PRIORITY, Feature
12 |
13 |
14 | class PytestConfig(Feature.Config):
15 | version = "~=4.6"
16 | """
17 | Pytest version to use in dev requirements. You can override this using `set_version(...)`.
18 | """
19 |
20 | addons = {"coverage": "~=4.5", "pytest-cov": "~=2.7"}
21 | """
22 | Additionnal packages to use in dev requirements along with the main pytest packe. You can override this dictionnary.
23 | """
24 |
25 | def set_version(self, version):
26 | """
27 | Overrides Pytest version requirement with your own.
28 | """
29 | self.version = version
30 |
31 |
32 | class PytestFeature(Feature):
33 | # TODO: http://docs.pytest.org/en/latest/goodpractices.html#integrating-with-setuptools-python-setup-py-test-pytest-runner
34 |
35 | Config = PytestConfig
36 |
37 | requires = {"python"}
38 |
39 | @subscribe("medikit.feature.python.on_generate")
40 | def on_python_generate(self, event):
41 | config = self.get_config(event)
42 | python = self.get_config(event, "python")
43 |
44 | python.add_requirements(dev=["pytest " + config.version, *map(" ".join, config.addons.items())])
45 |
46 | @subscribe("medikit.feature.make.on_generate", priority=SUPPORT_PRIORITY)
47 | def on_make_generate(self, event):
48 | makefile = event.makefile
49 | makefile["PYTEST"] = "$(PYTHON_DIRNAME)/pytest"
50 | makefile["PYTEST_OPTIONS"] = "--capture=no --cov=$(PACKAGE) --cov-report html".format(
51 | path=event.package_name.replace(".", os.sep)
52 | )
53 | makefile.add_target(
54 | "test",
55 | """
56 | $(PYTEST) $(PYTEST_OPTIONS) tests
57 | """,
58 | deps=("install-dev",),
59 | phony=True,
60 | doc="Runs the test suite.",
61 | )
62 |
63 | @subscribe(medikit.on_start, priority=SUPPORT_PRIORITY)
64 | def on_start(self, event):
65 | tests_dir = "tests"
66 | if not os.path.exists(tests_dir):
67 | os.makedirs(tests_dir)
68 |
69 | gitkeep_file = os.path.join(tests_dir, ".gitkeep")
70 |
71 | if not os.path.exists(gitkeep_file):
72 | self.render_empty_files(gitkeep_file)
73 |
74 | self.render_file(".coveragerc", "pytest/coveragerc.j2")
75 | self.render_file(".travis.yml", "pytest/travis.yml.j2")
76 |
--------------------------------------------------------------------------------
/medikit/feature/sphinx.py:
--------------------------------------------------------------------------------
1 | from pip._vendor.distlib.util import parse_requirement
2 |
3 | from medikit.events import subscribe
4 |
5 | from . import SUPPORT_PRIORITY, Feature
6 |
7 |
8 | class SphinxConfig(Feature.Config):
9 | def __init__(self):
10 | self._theme = None
11 |
12 | def set_theme(self, theme):
13 | """
14 | Sets the theme. Prefer using the .theme property.
15 |
16 | :param theme: Requirement for theme
17 | """
18 | self._theme = parse_requirement(theme)
19 |
20 | def get_theme(self):
21 | """
22 | Gets the theme. Prefer using the .theme property.
23 |
24 | :return: parsed requirement
25 | """
26 | return self._theme
27 |
28 | theme = property(
29 | fget=get_theme, fset=set_theme, doc="Sphinx theme to use, that should be parsable as a requirement."
30 | )
31 |
32 | version = "~=3.4"
33 | """
34 | Sphinx version to use. You can override this using `set_version(...)`.
35 | """
36 |
37 | def set_version(self, version):
38 | """Overrides package version."""
39 | self.version = version
40 |
41 | with_autobuild = False
42 |
43 |
44 | class SphinxFeature(Feature):
45 | Config = SphinxConfig
46 |
47 | @subscribe("medikit.feature.python.on_generate")
48 | def on_python_generate(self, event):
49 | sphinx_config: SphinxConfig = self.get_config(event)
50 | python_config = self.get_config(event, "python")
51 |
52 | python_config.add_requirements(dev=["sphinx " + sphinx_config.version])
53 | theme = sphinx_config.get_theme()
54 | if theme:
55 | python_config.add_requirements(dev=[theme.requirement])
56 |
57 | @subscribe("medikit.feature.make.on_generate", priority=SUPPORT_PRIORITY)
58 | def on_make_generate(self, event):
59 | makefile = event.makefile
60 |
61 | makefile["SPHINX_BUILD"] = "$(PYTHON_DIRNAME)/sphinx-build"
62 | makefile["SPHINX_OPTIONS"] = ""
63 | makefile["SPHINX_SOURCEDIR"] = "docs"
64 | makefile["SPHINX_BUILDDIR"] = "$(SPHINX_SOURCEDIR)/_build"
65 |
66 | makefile.add_target(
67 | "$(SPHINX_SOURCEDIR)",
68 | """
69 | $(SPHINX_BUILD) -b html -D latex_paper_size=a4 $(SPHINX_OPTIONS) $(SPHINX_SOURCEDIR) $(SPHINX_BUILDDIR)/html
70 | """,
71 | deps=("install-dev",),
72 | phony=True,
73 | )
74 |
75 | # optionnal feature: autobuild
76 | sphinx_config: SphinxConfig = self.get_config(event)
77 | if sphinx_config.with_autobuild:
78 | # Sphinx
79 | makefile["SPHINX_AUTOBUILD"] = "$(PYTHON_DIRNAME)/sphinx-autobuild"
80 | makefile.add_target(
81 | "docs-autobuild",
82 | "$(SPHINX_AUTOBUILD) --port 8001 $(SPHINX_SOURCEDIR) $(shell mktemp -d)",
83 | phony=True,
84 | doc="Run a webserver for sphinx documentation, with autoreload (requires sphinx-autobuild).",
85 | )
86 |
--------------------------------------------------------------------------------
/medikit/feature/template/Projectfile.j2:
--------------------------------------------------------------------------------
1 | # {{ name }} (see github.com/python-medikit)
2 |
3 | from medikit import require
4 |
5 | NAME = '{{ name }}'
6 |
7 | with require('python') as python:
8 | python.setup(
9 | name = NAME,
10 | description = '{{ description }}',
11 | license = '{{ license }}',
12 | url = '{{ url }}',
13 | download_url = '{{ download_url }}',
14 | author = '{{ author }}',
15 | author_email = '{{ author_email }}',
16 | )
17 |
18 | python.add_requirements(
19 | # Insert your requirements here. We suggest you use minor version requirements here, and freeze an exact version in
20 | # requirements*.txt (which, when does not exist, can be generated by medikit).
21 | {% for requirement in requirements %}
22 | '{{ requirement }}',
23 | {% endfor %}
24 | )
25 |
26 | {%- for feature in features | sort %}
27 |
28 | with require('{{ feature }}') as {{ feature }}:
29 | pass
30 | {%- endfor %}
31 |
32 | # vim: ft=python:
33 |
--------------------------------------------------------------------------------
/medikit/feature/template/console_script/__main__.py.j2:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # coding=utf-8
3 |
4 | from __future__ import unicode_literals
5 |
6 | import sys
7 | from argparse import ArgumentParser as BaseArgumentParser
8 |
9 |
10 | class ArgumentParser(BaseArgumentParser):
11 | pass
12 |
13 |
14 | def main(args=None, prog=None):
15 | args = args or sys.argv[1:]
16 | prog = prog or sys.argv[0]
17 |
18 | raise NotImplementedError(
19 | 'Project command line interface is up and running, but you should probably add some code now.'
20 | )
21 |
22 |
23 | if __name__ == '__main__':
24 | main()
25 |
--------------------------------------------------------------------------------
/medikit/feature/template/django/Makefile:
--------------------------------------------------------------------------------
1 | DJANGO_VERSION = 2.0.x
2 | BASE_URL ?= https://raw.githubusercontent.com/django/django/stable/$(DJANGO_VERSION)/django/conf/project_template
3 |
4 | .PHONY: all clean
5 |
6 | all: urls.py.j2 wsgi.py.j2 settings.py.j2 manage.py.j2
7 | rm -f *.py-tpl
8 |
9 | %.py.j2: %.py-tpl
10 | sed -E 's/{{[[:space:]]*project_name }}.(settings|urls|wsgi)/{{ config_package }}.\1/g' $< > $@
11 |
12 | manage.py-tpl:
13 | wget $(BASE_URL)/$@ -O $@
14 |
15 | %.py-tpl:
16 | wget $(BASE_URL)/project_name/$@ -O $@
17 |
18 | clean:
19 | rm -f *.j2
20 | rm -f *.py-tpl
21 |
22 |
23 |
--------------------------------------------------------------------------------
/medikit/feature/template/django/manage.py.j2:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ config_package }}.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/medikit/feature/template/django/settings.py.j2:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for {{ project_name }} project.
3 |
4 | Generated by 'django-admin startproject' using Django {{ django_version }}.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '{{ secret_key }}'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | ]
41 |
42 | MIDDLEWARE = [
43 | 'django.middleware.security.SecurityMiddleware',
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.common.CommonMiddleware',
46 | 'django.middleware.csrf.CsrfViewMiddleware',
47 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
48 | 'django.contrib.messages.middleware.MessageMiddleware',
49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
50 | ]
51 |
52 | ROOT_URLCONF = '{{ config_package }}.urls'
53 |
54 | TEMPLATES = [
55 | {
56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
57 | 'DIRS': [],
58 | 'APP_DIRS': True,
59 | 'OPTIONS': {
60 | 'context_processors': [
61 | 'django.template.context_processors.debug',
62 | 'django.template.context_processors.request',
63 | 'django.contrib.auth.context_processors.auth',
64 | 'django.contrib.messages.context_processors.messages',
65 | ],
66 | },
67 | },
68 | ]
69 |
70 | WSGI_APPLICATION = '{{ config_package }}.wsgi.application'
71 |
72 |
73 | # Database
74 | # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
75 |
76 | DATABASES = {
77 | 'default': {
78 | 'ENGINE': 'django.db.backends.sqlite3',
79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
80 | }
81 | }
82 |
83 |
84 | # Password validation
85 | # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
86 |
87 | AUTH_PASSWORD_VALIDATORS = [
88 | {
89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
90 | },
91 | {
92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
93 | },
94 | {
95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
96 | },
97 | {
98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
99 | },
100 | ]
101 |
102 |
103 | # Internationalization
104 | # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
105 |
106 | LANGUAGE_CODE = 'en-us'
107 |
108 | TIME_ZONE = 'UTC'
109 |
110 | USE_I18N = True
111 |
112 | USE_L10N = True
113 |
114 | USE_TZ = True
115 |
116 |
117 | # Static files (CSS, JavaScript, Images)
118 | # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
119 |
120 | STATIC_URL = '/static/'
121 |
--------------------------------------------------------------------------------
/medikit/feature/template/django/urls.py.j2:
--------------------------------------------------------------------------------
1 | """{{ project_name }} URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/{{ docs_version }}/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path
18 |
19 | urlpatterns = [
20 | path('admin/', admin.site.urls),
21 | ]
22 |
--------------------------------------------------------------------------------
/medikit/feature/template/django/wsgi.py.j2:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for {{ project_name }} project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ config_package }}.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/medikit/feature/template/pytest/coveragerc.j2:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 |
4 | [report]
5 | # Regexes for lines to exclude from consideration
6 | exclude_lines =
7 | # Have to re-enable the standard pragma
8 | pragma: no cover
9 |
10 | # Don't complain about missing debug-only code:
11 | def __repr__
12 | if self\.debug
13 |
14 | # Don't complain if tests don't hit defensive assertion code:
15 | raise AbstractError
16 | raise AssertionError
17 | raise NotImplementedError
18 |
19 | # Don't complain if non-runnable code isn't run:
20 | if 0:
21 | if __name__ == .__main__.:
22 |
23 | ignore_errors = True
24 |
25 | [html]
26 | directory = docs/_build/html/coverage
27 |
--------------------------------------------------------------------------------
/medikit/feature/template/pytest/travis.yml.j2:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - 3.5
4 | - 3.5-dev
5 | - 3.6
6 | - 3.6-dev
7 | - nightly
8 | install:
9 | - make install-dev
10 | - pip install coveralls
11 | script:
12 | - make clean test
13 | after_success:
14 | - coveralls
--------------------------------------------------------------------------------
/medikit/feature/template/python/package_init.py.j2:
--------------------------------------------------------------------------------
1 | {% if is_namespace %}
2 | # this is a namespace package
3 | try:
4 | import pkg_resources
5 | pkg_resources.declare_namespace(__name__)
6 | except ImportError:
7 | import pkgutil
8 | __path__ = pkgutil.extend_path(__path__, __name__)
9 | {% endif %}
10 |
--------------------------------------------------------------------------------
/medikit/feature/template/python/setup.py.j2:
--------------------------------------------------------------------------------
1 | {{ banner }}
2 |
3 | from setuptools import setup, find_packages
4 | from codecs import open
5 | from os import path
6 |
7 | here = path.abspath(path.dirname(__file__))
8 |
9 | # Py3 compatibility hacks, borrowed from IPython.
10 | try:
11 | execfile
12 | except NameError:
13 | def execfile(fname, globs, locs=None):
14 | locs = locs or globs
15 | exec(compile(open(fname).read(), fname, "exec"), globs, locs)
16 |
17 | # Get the long description from the README file
18 | try:
19 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
20 | long_description = f.read()
21 | except:
22 | long_description = ''
23 |
24 | # Get the classifiers from the classifiers file
25 | tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split('\n'))))
26 | try:
27 | with open(path.join(here, 'classifiers.txt'), encoding='utf-8') as f:
28 | classifiers = tolines(f.read())
29 | except:
30 | classifiers = []
31 |
32 | version_ns = {}
33 | try:
34 | execfile(path.join(here, '{{ python.version_file }}'), version_ns)
35 | except EnvironmentError:
36 | version = 'dev'
37 | else:
38 | version = version_ns.get('__version__', 'dev')
39 |
40 | setup(
41 | {% for k, v in setup.items()|sort -%}
42 | {{ k }} = {{ v | pprint }},{{ '\n ' }}
43 | {%- endfor -%}
44 | version = version,
45 | long_description = long_description,
46 | classifiers = classifiers,
47 | {% if not 'packages' in setup -%}
48 | packages = find_packages(exclude=['ez_setup', 'example', 'test']),
49 | {%- endif -%}
50 | include_package_data = True,
51 | {% if install_requires -%}
52 | install_requires = {{ install_requires | pprint }},{{ '\n ' }}
53 | {%- endif -%}
54 | {% if extras_require -%}
55 | extras_require = {{ extras_require | pprint }},{{ '\n ' }}
56 | {%- endif -%}
57 | {% if entry_points -%}
58 | entry_points = {{ entry_points | pprint }},{{ '\n ' }}
59 | {%- endif -%}
60 | url = {{ url | pprint }},
61 | download_url = {{ download_url | pprint }}.format(version=version),
62 | )
63 |
--------------------------------------------------------------------------------
/medikit/feature/template/yapf/style.yapf.j2:
--------------------------------------------------------------------------------
1 | [style]
2 | based_on_style = pep8
3 | column_limit = 120
4 | dedent_closing_brackets = true
5 |
--------------------------------------------------------------------------------
/medikit/feature/webpack.py:
--------------------------------------------------------------------------------
1 | """
2 | Webpack support.
3 |
4 | This feature is experimental and as though it may work for you, that's not a guarantee. Please use with care.
5 |
6 | """
7 |
8 | from medikit.events import subscribe
9 |
10 | from . import Feature
11 |
12 |
13 | class WebpackFeature(Feature):
14 | requires = {"nodejs"}
15 |
16 | @subscribe("medikit.feature.make.on_generate")
17 | def on_make_generate(self, event):
18 | event.makefile.get_target("install").install.append("$(YARN) --version")
19 |
--------------------------------------------------------------------------------
/medikit/feature/yapf.py:
--------------------------------------------------------------------------------
1 | """
2 | YAPF support, to automatically reformat all your (python) source code.
3 |
4 | .. code-block:: shell-session
5 |
6 | $ make format
7 |
8 | """
9 |
10 | import os
11 | import warnings
12 |
13 | import medikit
14 | from medikit import settings
15 | from medikit.events import subscribe
16 | from medikit.feature import ABSOLUTE_PRIORITY, SUPPORT_PRIORITY, Feature
17 | from medikit.structs import Script
18 |
19 |
20 | class YapfFeature(Feature):
21 | requires = {"python"}
22 | conflicts = {"format"}
23 |
24 | def __init__(self, dispatcher):
25 | super().__init__(dispatcher)
26 | warnings.warn(
27 | 'The "yapf" feature is deprecated, please switch to "format" feature and call .using("yapf") on its configuration object.',
28 | DeprecationWarning,
29 | )
30 |
31 | @subscribe("medikit.feature.python.on_generate")
32 | def on_python_generate(self, event):
33 | event.config["python"].add_requirements(dev=["yapf"])
34 |
35 | @subscribe("medikit.feature.make.on_generate", priority=SUPPORT_PRIORITY)
36 | def on_make_generate(self, event):
37 | makefile = event.makefile
38 | makefile["YAPF"] = "$(PYTHON) -m yapf"
39 | makefile["YAPF_OPTIONS"] = "-rip"
40 | makefile.add_target(
41 | "format",
42 | Script("\n".join(["$(YAPF) $(YAPF_OPTIONS) .", "$(YAPF) $(YAPF_OPTIONS) Projectfile"])),
43 | deps=("install-dev",),
44 | phony=True,
45 | doc="Reformats the whole python codebase using yapf.",
46 | )
47 |
48 | @subscribe(medikit.on_start, priority=SUPPORT_PRIORITY)
49 | def on_start(self, event):
50 | self.render_file(".style.yapf", "yapf/style.yapf.j2")
51 |
52 | @subscribe(medikit.on_start, priority=ABSOLUTE_PRIORITY - 1)
53 | def on_before_start(self, event):
54 | style_config = os.path.join(os.getcwd(), ".style.yapf")
55 | if os.path.exists(style_config):
56 | self.dispatcher.info("YAPF_STYLE_CONFIG = " + style_config)
57 | settings.YAPF_STYLE_CONFIG = style_config
58 |
--------------------------------------------------------------------------------
/medikit/file.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import os
3 | import stat
4 |
5 | from whistle import Event
6 |
7 | ENCODING = "utf-8"
8 |
9 |
10 | class FileEvent(Event):
11 | def __init__(self, filename, executable, override):
12 | super(FileEvent, self).__init__()
13 | self.executable = executable
14 | self.filename = filename
15 | self.override = override
16 |
17 |
18 | @contextlib.contextmanager
19 | def File(dispatcher, name, *, executable=False, override=False):
20 | event = FileEvent(name, executable, override)
21 |
22 | if event.override or not os.path.exists(event.filename):
23 | with open(event.filename, "w+", encoding=ENCODING) as f:
24 | event.file = f
25 | event = dispatcher.dispatch("medikit.on_file_opened", event)
26 | yield f
27 | event.file = None
28 |
29 | if event.executable and os.path.exists(event.filename):
30 | st = os.stat(event.filename)
31 | os.chmod(event.filename, st.st_mode | stat.S_IEXEC)
32 |
33 | dispatcher.dispatch("medikit.on_file_closed", event)
34 | else:
35 | with open("/dev/null", "w", encoding=ENCODING) as f:
36 | yield f
37 |
38 |
39 | @contextlib.contextmanager
40 | def NullFile(dispatcher, name, *, executable=False, override=False):
41 | event = FileEvent(name, executable, override)
42 |
43 | if event.override or not os.path.exists(event.filename):
44 | with open("/dev/null", "w", encoding=ENCODING) as f:
45 | event.file = f
46 | event = dispatcher.dispatch("medikit.on_file_opened", event)
47 | yield f
48 | event.file = None
49 |
50 | dispatcher.dispatch("medikit.on_file_closed", event)
51 | else:
52 | with open("/dev/null", "w", encoding=ENCODING) as f:
53 | yield f
54 |
--------------------------------------------------------------------------------
/medikit/globals.py:
--------------------------------------------------------------------------------
1 | PIP_VERSION = ">=19,<20"
2 | LINE_LENGTH = "120"
3 |
--------------------------------------------------------------------------------
/medikit/pipeline.py:
--------------------------------------------------------------------------------
1 | """
2 | Pipelines are a way to describe a simple step-by-step process, for example the release process.
3 |
4 | """
5 | import datetime
6 | import json
7 | import logging
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def get_identity(step):
13 | return str(step)
14 |
15 |
16 | class Pipeline:
17 | """
18 | Class to configure a pipeline.
19 |
20 | """
21 |
22 | def __init__(self):
23 | self.steps = []
24 |
25 | def add(self, step, *, before=None):
26 | if before:
27 | insert_at = None
28 | for i, _step in enumerate(self.steps):
29 | if before == get_identity(_step):
30 | insert_at = i
31 | break
32 | if not insert_at:
33 | raise ValueError(
34 | 'Step with identity {!r} not found. Try "show" subcommand to list identities.'.format(before)
35 | )
36 | self.steps = self.steps[:insert_at] + [step] + self.steps[insert_at:]
37 | else:
38 | self.steps.append(step)
39 | return self
40 |
41 | def remove(self, identity):
42 | for i in range(len(self.steps)):
43 | if identity == get_identity(self.steps[i]):
44 | del self.steps[i]
45 | break
46 |
47 | def __iter__(self):
48 | yield from self.steps
49 |
50 |
51 | class ConfiguredPipeline:
52 | """
53 | Used to actually load run and persist a configured pipeline.
54 | """
55 |
56 | def __init__(self, name, pipeline, config=None):
57 | self.name = name
58 | self.steps = pipeline.steps
59 | self.meta = {"created": str(datetime.datetime.now())}
60 | self.config = config
61 |
62 | def __iter__(self):
63 | yield from self.steps
64 |
65 | def __len__(self):
66 | return len(self.steps)
67 |
68 | def init(self):
69 | for step in self.steps:
70 | step.init()
71 |
72 | def next(self):
73 | for step in self.steps:
74 | if not step.complete:
75 | return step
76 | raise StopIteration("No step left.")
77 |
78 | @property
79 | def current(self):
80 | for i, step in enumerate(self.steps):
81 | if not step.complete:
82 | return i + 1
83 | return len(self)
84 |
85 | def abort(self):
86 | for step in self.steps:
87 | step.abort()
88 |
89 | def serialize(self):
90 | return json.dumps(
91 | {
92 | "meta": {**self.meta, "updated": str(datetime.datetime.now())},
93 | "steps": [[get_identity(step), step.get_state()] for step in self.steps],
94 | },
95 | indent=4,
96 | )
97 |
98 | def unserialize(self, serialized):
99 | serialized = json.loads(serialized)
100 | self.meta = serialized.get("meta", {})
101 | steps = serialized.get("steps", [])
102 | if len(steps) != len(self.steps):
103 | raise IOError("Invalid pipeline state storage.")
104 | for (identity, state), step in zip(steps, self.steps):
105 | if get_identity(step) != identity:
106 | raise IOError("Mismatch on step identity.")
107 | step.set_state(state)
108 | step.config = self.config
109 |
--------------------------------------------------------------------------------
/medikit/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-medikit/medikit/db78a4b6a912ccff818eb3428626a204e803f77c/medikit/resources/__init__.py
--------------------------------------------------------------------------------
/medikit/resources/configparser.py:
--------------------------------------------------------------------------------
1 | import configparser
2 | import os
3 |
4 |
5 | class AbstractResource:
6 | def __enter__(self):
7 | return self
8 |
9 | def __exit__(self, exc_type, exc_val, exc_tb):
10 | pass
11 |
12 |
13 | def _set_values(c, values):
14 | for k, v in values.items():
15 | if not k in c:
16 | c[k] = {}
17 | for kk, vv in v.items():
18 | c[k][kk] = vv
19 |
20 |
21 | class ConfigParserResource(AbstractResource):
22 | def __init__(self):
23 | self.initial_values = []
24 | self.managed_values = []
25 |
26 | def set_initial_values(self, values):
27 | self.initial_values.append(values)
28 |
29 | def set_managed_values(self, values):
30 | self.managed_values.append(values)
31 |
32 | def write(self, event, target):
33 | config = configparser.ConfigParser()
34 | exists = os.path.exists(target)
35 | if exists:
36 | config.read(target)
37 | else:
38 | # only apply to new files
39 | for initial_value in self.initial_values:
40 | _set_values(config, initial_value)
41 |
42 | for managed_value in self.managed_values:
43 | _set_values(config, managed_value)
44 |
45 | with open(target, "w+") as f:
46 | config.write(f)
47 |
--------------------------------------------------------------------------------
/medikit/settings.py:
--------------------------------------------------------------------------------
1 | DEFAULT_FEATURES = {"git", "make"}
2 |
3 | DEFAULT_FILES = {"requirements", "requirements-dev", "classifiers", "version"}
4 |
5 | YAPF_STYLE_CONFIG = "pep8"
6 |
--------------------------------------------------------------------------------
/medikit/steps/__init__.py:
--------------------------------------------------------------------------------
1 | from medikit.steps.base import Step
2 | from medikit.steps.exec import Commit, Make, System
3 | from medikit.steps.install import Install
4 | from medikit.steps.version import BumpVersion
5 |
6 | __all__ = ["BumpVersion", "Install", "Make", "Commit", "System", "Step"]
7 |
--------------------------------------------------------------------------------
/medikit/steps/base.py:
--------------------------------------------------------------------------------
1 | import shlex
2 | import subprocess
3 | from logging import getLogger
4 |
5 | from medikit.utils import run_command
6 |
7 |
8 | class Step:
9 | @property
10 | def logger(self):
11 | try:
12 | return self._logger
13 | except AttributeError:
14 | self._logger = getLogger(self.__class__.__module__)
15 | return self._logger
16 |
17 | @property
18 | def complete(self):
19 | return self._state.get("complete", False)
20 |
21 | def set_complete(self, value=True):
22 | self._state["complete"] = bool(value)
23 |
24 | def __init__(self):
25 | self._state = {}
26 | self.__args__ = ()
27 | self.config = None
28 |
29 | def __str__(self):
30 | return "{}({})".format(type(self).__name__, ", ".join(map(repr, self.__args__)))
31 |
32 | def get_state(self):
33 | return self._state
34 |
35 | def set_state(self, state):
36 | self._state = state
37 |
38 | def init(self):
39 | pass
40 |
41 | def run(self, meta):
42 | self.set_complete()
43 |
44 | def abort(self):
45 | pass
46 |
47 | def exec(self, command):
48 | return run_command(command, logger=self.logger)
49 |
--------------------------------------------------------------------------------
/medikit/steps/exec.py:
--------------------------------------------------------------------------------
1 | import json
2 | import multiprocessing
3 | import os
4 | from queue import Empty
5 |
6 | from git import Repo
7 | from mondrian import term
8 |
9 | from medikit.steps import Step
10 | from medikit.steps.utils.process import Process
11 |
12 |
13 | class System(Step):
14 | def __init__(self, cmd, *, interractive=False):
15 | super().__init__()
16 | self.cmd = cmd
17 | self.interractive = interractive
18 | self.__args__ = (cmd, interractive)
19 |
20 | def run(self, meta):
21 | if self.interractive:
22 | os.system(self.cmd)
23 | else:
24 | child = Process(self.cmd)
25 | events = multiprocessing.Queue()
26 | parent = multiprocessing.Process(name="task", target=child.run, args=(events, True))
27 | parent.start()
28 | exit = False
29 | returncode = None
30 | while not exit:
31 | try:
32 | msg = events.get(timeout=0.1)
33 | except Empty:
34 | if exit:
35 | break
36 | else:
37 | if msg.type == "line":
38 | print(term.lightblack("\u2502"), msg.data.decode("utf-8"), end="")
39 | elif msg.type == "start":
40 | print("$ " + term.lightwhite(self.cmd) + term.black(" # pid=%s" % msg.data["pid"]))
41 | elif msg.type == "stop":
42 | returncode = msg.data["returncode"]
43 | if returncode:
44 | print(term.lightblack("\u2514" + term.red(" failed (rc={}). ".format(returncode))))
45 | else:
46 | print(term.lightblack("\u2514" + term.green(" success. ")))
47 | exit = True
48 | if returncode:
49 | raise RuntimeError(
50 | '"{command}" exited with status {returncode}.'.format(command=self.cmd, returncode=returncode)
51 | )
52 | self.set_complete()
53 |
54 |
55 | class Make(System):
56 | def __init__(self, target):
57 | super().__init__("make " + target)
58 | self.__args__ = (target,)
59 |
60 |
61 | class Commit(Step):
62 | def __init__(self, message, *, tag=False):
63 | super().__init__()
64 | self.message = message
65 | self.tag = bool(tag)
66 | self.__args__ = (message, self.tag)
67 |
68 | def run(self, meta):
69 | branch = self.exec("git rev-parse --abbrev-ref HEAD")
70 | version = self.config.get_version()
71 | assert version == meta["version"]
72 | os.system("git commit -m " + json.dumps(self.message.format(**meta)))
73 | if self.tag:
74 | os.system("git tag -am {version} {version}".format(**meta))
75 |
76 | repo = Repo()
77 | for remote in repo.remotes:
78 | if str(remote) in ("origin", "upstream"):
79 | self.logger.info("git push {} {}...".format(remote, branch))
80 | os.system("git push {} {} --tags".format(remote, branch))
81 |
82 | self.set_complete()
83 |
--------------------------------------------------------------------------------
/medikit/steps/install.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from medikit.globals import PIP_VERSION
4 | from medikit.steps.exec import System
5 |
6 |
7 | class Install(System):
8 | def __init__(self, *packages):
9 | if not len(packages):
10 | packages = ("pip " + PIP_VERSION, "wheel", "twine")
11 | packages = list(sorted(packages))
12 | super().__init__(sys.executable + " -m pip install --upgrade " + " ".join(map(repr, packages)))
13 | self.__args__ = (*packages,)
14 |
--------------------------------------------------------------------------------
/medikit/steps/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-medikit/medikit/db78a4b6a912ccff818eb3428626a204e803f77c/medikit/steps/utils/__init__.py
--------------------------------------------------------------------------------
/medikit/steps/utils/process.py:
--------------------------------------------------------------------------------
1 | # This is stolen from honcho, probably needs refactoring / simplification for our usage.
2 | import datetime
3 | import os
4 | import signal
5 | import subprocess
6 | import sys
7 | from collections import namedtuple
8 |
9 | Message = namedtuple("Message", "type data time name colour")
10 |
11 |
12 | class Process(object):
13 | def __init__(self, cmd, name=None, colour=None, quiet=False, env=None, cwd=None):
14 | self.cmd = cmd
15 | self.colour = colour
16 | self.quiet = quiet
17 | self.name = name
18 | self.env = os.environ.copy() if env is None else env
19 | self.cwd = cwd
20 |
21 | self._child = None
22 | self._child_ctor = Popen
23 |
24 | def run(self, events=None, ignore_signals=False):
25 | self._events = events
26 | self._child = self._child_ctor(self.cmd, env=self.env, cwd=self.cwd)
27 | self._send_message({"pid": self._child.pid}, type="start")
28 |
29 | # Don't pay attention to SIGINT/SIGTERM. The process itself is
30 | # considered unkillable, and will only exit when its child (the shell
31 | # running the Procfile process) exits.
32 | if ignore_signals:
33 | signal.signal(signal.SIGINT, signal.SIG_IGN)
34 | signal.signal(signal.SIGTERM, signal.SIG_IGN)
35 |
36 | for line in iter(self._child.stdout.readline, b""):
37 | if not self.quiet:
38 | self._send_message(line)
39 | self._child.stdout.close()
40 | self._child.wait()
41 |
42 | self._send_message({"returncode": self._child.returncode}, type="stop")
43 |
44 | def _send_message(self, data, type="line"):
45 | if self._events is not None:
46 | self._events.put(
47 | Message(type=type, data=data, time=datetime.datetime.now(), name=self.name, colour=self.colour)
48 | )
49 |
50 |
51 | ON_WINDOWS = "win32" in str(sys.platform).lower()
52 |
53 |
54 | class Popen(subprocess.Popen):
55 | def __init__(self, cmd, **kwargs):
56 | start_new_session = kwargs.pop("start_new_session", True)
57 | options = {
58 | "stdout": subprocess.PIPE,
59 | "stderr": subprocess.STDOUT,
60 | "shell": True,
61 | "bufsize": 1,
62 | "close_fds": not ON_WINDOWS,
63 | }
64 | options.update(**kwargs)
65 |
66 | if ON_WINDOWS:
67 | # MSDN reference:
68 | # http://msdn.microsoft.com/en-us/library/windows/desktop/ms684863%28v=vs.85%29.aspx
69 | create_new_process_group = 0x00000200
70 | detached_process = 0x00000008
71 | options.update(creationflags=detached_process | create_new_process_group)
72 | elif start_new_session:
73 | if sys.version_info < (3, 2):
74 | options.update(preexec_fn=os.setsid)
75 | else:
76 | options.update(start_new_session=True)
77 |
78 | super(Popen, self).__init__(cmd, **options)
79 |
--------------------------------------------------------------------------------
/medikit/steps/version.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from git import Repo
4 | from git_semver import get_current_version
5 | from semantic_version import Version
6 |
7 | from medikit.steps import Step
8 |
9 |
10 | class PythonVersion(Version):
11 | def __str__(self):
12 | version = "%d" % self.major
13 | if self.minor is not None:
14 | version = "%s.%d" % (version, self.minor)
15 | if self.patch is not None:
16 | version = "%s.%d" % (version, self.patch)
17 |
18 | if self.prerelease or (self.partial and self.prerelease == () and self.build is None):
19 | version = "%s%s" % (version, ".".join(self.prerelease))
20 | if self.build or (self.partial and self.build == ()):
21 | version = "%s+%s" % (version, ".".join(self.build))
22 | return version
23 |
24 |
25 | class BumpVersion(Step):
26 | def get_name(self):
27 | try:
28 | # TODO see if we can ignore this, left here for BC
29 | return self.exec("python setup.py --name")
30 | except RuntimeError:
31 | return self.config.get_name()
32 |
33 | def run(self, meta):
34 | """
35 | git add $VERSION_FILE
36 | """
37 | name = self.get_name()
38 | version_file = self.config.get_version_file()
39 |
40 | # todo move this in config ?
41 | if not os.path.exists(version_file):
42 | raise FileNotFoundError("Cannot find version file for {} (searched in {!r}).".format(name, version_file))
43 |
44 | repo = Repo()
45 | for remote in repo.remotes:
46 | self.logger.info("git fetch {}...".format(remote))
47 | remote.fetch(tags=True)
48 |
49 | git_version = get_current_version(repo, Version=PythonVersion)
50 | if git_version:
51 | git_version.partial = False
52 |
53 | current_version = PythonVersion.coerce(self.config.get_version(), partial=True)
54 | current_version.partial = False
55 |
56 | next_version = None
57 | while not next_version:
58 | try:
59 | print("Current version:", current_version, "Git version:", git_version)
60 | next_version = input("Next version? ")
61 | # todo next patch, etc.
62 | next_version = PythonVersion.coerce(next_version, partial=True)
63 | next_version.partial = False
64 | except ValueError as exc:
65 | self.logger.error(exc)
66 | next_version = None
67 |
68 | with open(version_file, "w+") as f:
69 | # Support both py format and txt version, let's move this logic to config at some point (TODO).
70 | if os.path.splitext(version_file)[1] == ".py":
71 | f.write("__version__ = '{}'\n".format(str(next_version)))
72 | else:
73 | f.write(str(next_version))
74 |
75 | self.exec("git add {}".format(version_file))
76 |
77 | meta["version"] = str(next_version)
78 |
79 | self.set_complete()
80 |
--------------------------------------------------------------------------------
/medikit/structs.py:
--------------------------------------------------------------------------------
1 | import textwrap
2 |
3 |
4 | class Script(object):
5 | """
6 | Simple structure to hold a shell script for various usage, mainly to use in make targets.
7 |
8 | The goal is to make it flexible, as in alow the user to ammend it after definition.
9 |
10 | """
11 |
12 | doc = None
13 |
14 | def __init__(self, script=None, *, doc=None):
15 | self.set(script)
16 | self.doc = doc
17 |
18 | def set(self, script=None):
19 | self.script = self.parse_script(script)
20 |
21 | def prepend(self, script=None):
22 | """
23 | Prepend a script to the current script.
24 |
25 | :param script:
26 | """
27 | self.script = self.parse_script(script) + self.script
28 |
29 | def append(self, script=None):
30 | """
31 | Append a script to the current script.
32 |
33 | :param script:
34 | """
35 | self.script = self.script + self.parse_script(script)
36 |
37 | def parse_script(self, script):
38 | if not script:
39 | return []
40 | script = textwrap.dedent(str(script)).strip()
41 | return script.split("\n")
42 |
43 | def __iter__(self):
44 | for line in self.script:
45 | yield line
46 |
47 | def __str__(self):
48 | return "\n".join(self.__iter__())
49 |
--------------------------------------------------------------------------------
/medikit/testing.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from medikit.config import ConfigurationRegistry
4 | from medikit.events import LoggingDispatcher
5 | from medikit.feature import Feature
6 | from medikit.file import NullFile
7 |
8 |
9 | class FeatureTestCase(TestCase):
10 | feature_type = Feature
11 | required_features = set()
12 |
13 | def create_dispatcher(self):
14 | return LoggingDispatcher()
15 |
16 | def create_feature(self, feature_type=None, dispatcher=None):
17 | dispatcher = dispatcher or self.create_dispatcher()
18 | feature = (feature_type or self.feature_type)(dispatcher)
19 | feature.file_type = NullFile
20 | return feature, dispatcher
21 |
22 | def create_config(self):
23 | config = ConfigurationRegistry(self.create_dispatcher())
24 | if len(self.required_features):
25 | config.require(*self.required_features)
26 | return config
27 |
--------------------------------------------------------------------------------
/medikit/utils.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import keyword
3 | import logging
4 | import shlex
5 | import subprocess
6 | import textwrap
7 |
8 | import medikit
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | def is_identifier(ident: str) -> bool:
14 | """Determines if string is valid Python identifier."""
15 |
16 | if not isinstance(ident, str):
17 | raise TypeError("expected str, but got {!r}".format(type(ident)))
18 |
19 | if not ident.isidentifier():
20 | return False
21 |
22 | if keyword.iskeyword(ident):
23 | return False
24 |
25 | return True
26 |
27 |
28 | def format_file_content(s):
29 | return textwrap.dedent(s).strip() + "\n"
30 |
31 |
32 | def get_override_warning_banner(*, prefix="# ", above=None, bellow=None):
33 | return "\n".join(
34 | filter(
35 | None,
36 | (
37 | above,
38 | textwrap.indent(
39 | "\n".join(
40 | (
41 | "Generated by Medikit "
42 | + medikit.__version__
43 | + " on "
44 | + str(datetime.datetime.now().date())
45 | + ".",
46 | "All changes will be overriden.",
47 | "Edit Projectfile and run “make update” (or “medikit update”) to regenerate.",
48 | )
49 | ),
50 | prefix=prefix,
51 | ),
52 | bellow,
53 | ),
54 | )
55 | )
56 |
57 |
58 | def run_command(command, *, logger=logger):
59 | logger.info("Running command %s", command)
60 | result = subprocess.run(shlex.split(command), stdout=subprocess.PIPE)
61 | if result.returncode:
62 | raise RuntimeError(
63 | '"{command}" exited with status {returncode}.'.format(command=command, returncode=result.returncode)
64 | )
65 | return result.stdout.decode("utf-8").strip()
66 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 120
3 | target-version = ['py35', 'py36', 'py37', 'py38']
4 | include = '\.pyi?$'
5 | exclude = '''
6 | /(
7 | \.eggs
8 | | \.git
9 | | \.hg
10 | | \.mypy_cache
11 | | \.tox
12 | | _build
13 | | build
14 | | dist
15 | )/
16 | '''
17 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -e .[dev]
2 | -r requirements.txt
3 | alabaster==0.7.12
4 | appdirs==1.4.4
5 | attrs==21.2.0
6 | babel==2.9.1
7 | backports.entry-points-selectable==1.1.0
8 | black==20.8b1
9 | certifi==2021.5.30
10 | cfgv==3.3.1
11 | charset-normalizer==2.0.4
12 | click==8.0.1
13 | coverage==5.5
14 | distlib==0.3.2
15 | docutils==0.16
16 | filelock==3.0.12
17 | identify==2.2.13
18 | idna==3.2
19 | imagesize==1.2.0
20 | importlib-metadata==4.8.1
21 | iniconfig==1.1.1
22 | isort==5.9.3
23 | jinja2==3.0.1
24 | markupsafe==2.0.1
25 | mypy-extensions==0.4.3
26 | nodeenv==1.6.0
27 | packaging==21.0
28 | pathspec==0.9.0
29 | platformdirs==2.3.0
30 | pluggy==1.0.0
31 | pre-commit==2.9.3
32 | py==1.10.0
33 | pygments==2.10.0
34 | pyparsing==2.4.7
35 | pytest-cov==2.12.1
36 | pytest==6.2.5
37 | pytz==2021.1
38 | pyyaml==5.4.1
39 | regex==2021.8.28
40 | releases==1.6.3
41 | requests==2.26.0
42 | semantic-version==2.6.0
43 | six==1.16.0
44 | snowballstemmer==2.1.0
45 | sphinx-sitemap==1.1.0
46 | sphinx==3.5.4
47 | sphinxcontrib-applehelp==1.0.2
48 | sphinxcontrib-devhelp==1.0.2
49 | sphinxcontrib-htmlhelp==2.0.0
50 | sphinxcontrib-jsmath==1.0.1
51 | sphinxcontrib-qthelp==1.0.3
52 | sphinxcontrib-serializinghtml==1.1.5
53 | toml==0.10.2
54 | typed-ast==1.4.3
55 | typing-extensions==3.10.0.2
56 | urllib3==1.26.6
57 | virtualenv==20.7.2
58 | zipp==3.5.0
59 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -e .
2 | click==8.0.1
3 | colorama==0.4.4
4 | git-semver==0.3.2
5 | gitdb==4.0.7
6 | gitpython==3.1.18
7 | importlib-metadata==4.8.1
8 | jinja2==2.11.3
9 | markupsafe==2.0.1
10 | mondrian==0.8.1
11 | packaging==20.9
12 | pbr==5.6.0
13 | pip-tools==4.5.1
14 | pyparsing==2.4.7
15 | semantic-version==2.6.0
16 | six==1.16.0
17 | smmap==4.0.0
18 | stevedore==3.4.0
19 | typing-extensions==3.10.0.2
20 | whistle==1.0.1
21 | yapf==0.31.0
22 | zipp==3.5.0
23 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [metadata]
5 | description-file = README.rst
6 |
7 | [isort]
8 | line_length = 120
9 |
10 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Generated by Medikit 0.8.0 on 2021-09-08.
2 | # All changes will be overriden.
3 | # Edit Projectfile and run “make update” (or “medikit update”) to regenerate.
4 |
5 | from codecs import open
6 | from os import path
7 |
8 | from setuptools import find_packages, setup
9 |
10 | here = path.abspath(path.dirname(__file__))
11 |
12 | # Py3 compatibility hacks, borrowed from IPython.
13 | try:
14 | execfile
15 | except NameError:
16 |
17 | def execfile(fname, globs, locs=None):
18 | locs = locs or globs
19 | exec(compile(open(fname).read(), fname, "exec"), globs, locs)
20 |
21 |
22 | # Get the long description from the README file
23 | try:
24 | with open(path.join(here, "README.rst"), encoding="utf-8") as f:
25 | long_description = f.read()
26 | except:
27 | long_description = ""
28 |
29 | # Get the classifiers from the classifiers file
30 | tolines = lambda c: list(filter(None, map(lambda s: s.strip(), c.split("\n"))))
31 | try:
32 | with open(path.join(here, "classifiers.txt"), encoding="utf-8") as f:
33 | classifiers = tolines(f.read())
34 | except:
35 | classifiers = []
36 |
37 | version_ns = {}
38 | try:
39 | execfile(path.join(here, "medikit/_version.py"), version_ns)
40 | except EnvironmentError:
41 | version = "dev"
42 | else:
43 | version = version_ns.get("__version__", "dev")
44 |
45 | setup(
46 | author="Romain Dorgueil",
47 | author_email="romain@dorgueil.net",
48 | description="Opinionated python 3.5+ project management.",
49 | license="Apache License, Version 2.0",
50 | name="medikit",
51 | python_requires=">=3.5",
52 | version=version,
53 | long_description=long_description,
54 | classifiers=classifiers,
55 | packages=find_packages(exclude=["ez_setup", "example", "test"]),
56 | include_package_data=True,
57 | install_requires=[
58 | "git-semver ~= 0.3.2",
59 | "jinja2 ~= 2.10",
60 | "mondrian ~= 0.8",
61 | "packaging ~= 20.0",
62 | "pip >= 19, < 20",
63 | "pip-tools ~= 4.5.0",
64 | "semantic_version < 2.7",
65 | "stevedore ~= 3.0",
66 | "whistle ~= 1.0",
67 | "yapf ~= 0.20",
68 | ],
69 | extras_require={
70 | "dev": [
71 | "black == 20.8b1",
72 | "coverage ~= 5.3",
73 | "isort",
74 | "pre-commit ~= 2.9.0",
75 | "pytest ~= 6.0",
76 | "pytest-cov ~= 2.7",
77 | "releases >= 1.6, < 1.7",
78 | "sphinx ~= 3.4",
79 | "sphinx-sitemap ~= 1.0",
80 | ]
81 | },
82 | entry_points={
83 | "console_scripts": ["medikit=medikit.__main__:main"],
84 | "medikit.feature": [
85 | "django = medikit.feature.django:DjangoFeature",
86 | "docker = medikit.feature.docker:DockerFeature",
87 | "format = medikit.feature.format:FormatFeature",
88 | "git = medikit.feature.git:GitFeature",
89 | "kube = medikit.feature.kube:KubeFeature",
90 | "make = medikit.feature.make:MakeFeature",
91 | "nodejs = medikit.feature.nodejs:NodeJSFeature",
92 | "pylint = medikit.feature.pylint:PylintFeature",
93 | "pytest = medikit.feature.pytest:PytestFeature",
94 | "python = medikit.feature.python:PythonFeature",
95 | "sphinx = medikit.feature.sphinx:SphinxFeature",
96 | "webpack = medikit.feature.webpack:WebpackFeature",
97 | "yapf = medikit.feature.yapf:YapfFeature",
98 | ],
99 | },
100 | url="https://python-medikit.github.io/",
101 | download_url="https://github.com/python-medikit/medikit/archive/{version}.tar.gz".format(version=version),
102 | )
103 |
--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/python-medikit/medikit/db78a4b6a912ccff818eb3428626a204e803f77c/tests/.gitkeep
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pytest
4 |
5 |
6 | @pytest.yield_fixture()
7 | def tmpwd(tmpdir):
8 | old_wd = os.getcwd()
9 | try:
10 | os.chdir(str(tmpdir))
11 | yield tmpdir
12 | finally:
13 | os.chdir(old_wd)
14 |
--------------------------------------------------------------------------------
/tests/feature/test_feature_docker.py:
--------------------------------------------------------------------------------
1 | import medikit
2 | from medikit.feature.docker import DockerFeature
3 | from medikit.feature.make import Makefile, MakefileEvent
4 | from medikit.testing import FeatureTestCase
5 |
6 | PACKAGE_NAME = "bar"
7 |
8 |
9 | class TestDockerFeature(FeatureTestCase):
10 | feature_type = DockerFeature
11 | required_features = {"make", "docker"}
12 |
13 | def test_configure(self):
14 | feature, dispatcher = self.create_feature()
15 | listeners = dispatcher.get_listeners()
16 |
17 | assert feature.on_make_generate in listeners["medikit.feature.make.on_generate"]
18 | assert feature.on_end in listeners[medikit.on_end]
19 |
20 | def test_issue71_override_image_and_default_builder(self):
21 | feature, dispatcher = self.create_feature()
22 |
23 | config = self.create_config()
24 | config["docker"].use_default_builder()
25 | config["docker"].set_remote(registry="example.com", user="acme", name="one")
26 |
27 | event = MakefileEvent(PACKAGE_NAME, Makefile(), config)
28 | feature.on_make_generate(event)
29 |
30 | assert event.makefile.environ["DOCKER_IMAGE"] == "example.com/acme/one"
31 |
32 | def test_issue71_override_image_and_rocker_builder(self):
33 | feature, dispatcher = self.create_feature()
34 |
35 | config = self.create_config()
36 | config["docker"].use_rocker_builder()
37 | config["docker"].set_remote(registry="example.com", user="acme", name="two")
38 |
39 | event = MakefileEvent(PACKAGE_NAME, Makefile(), config)
40 | feature.on_make_generate(event)
41 |
42 | assert event.makefile.environ["DOCKER_IMAGE"] == "example.com/acme/two"
43 |
44 | def test_issue71_override_image_without_builder_override(self):
45 | feature, dispatcher = self.create_feature()
46 |
47 | config = self.create_config()
48 | config["docker"].set_remote(registry="example.com", user="acme", name="three")
49 |
50 | event = MakefileEvent(PACKAGE_NAME, Makefile(), config)
51 | feature.on_make_generate(event)
52 |
53 | assert event.makefile.environ["DOCKER_IMAGE"] == "example.com/acme/three"
54 |
--------------------------------------------------------------------------------
/tests/feature/test_feature_git.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | import pytest
4 |
5 | import medikit
6 | from medikit.events import ProjectEvent
7 | from medikit.feature.git import GitFeature
8 | from medikit.testing import FeatureTestCase
9 |
10 | PACKAGE_NAME = "foo.bar"
11 |
12 |
13 | class TestGitFeature(FeatureTestCase):
14 | feature_type = GitFeature
15 | required_features = {"git"}
16 |
17 | def test_configure(self):
18 | feature, dispatcher = self.create_feature()
19 | listeners = dispatcher.get_listeners()
20 |
21 | assert feature.on_start in listeners[medikit.on_start]
22 | assert feature.on_end in listeners[medikit.on_end]
23 |
24 | def test_on_start(self):
25 | feature, dispatcher = self.create_feature()
26 |
27 | config = self.create_config()
28 |
29 | with patch("os.path.exists", return_value=False):
30 | commands = list()
31 | with patch("os.system", side_effect=commands.append) as os_system:
32 | feature.on_start(ProjectEvent(config=config))
33 | assert commands == [
34 | "git init --quiet",
35 | "git add Projectfile",
36 | 'git commit --quiet -m "Project initialized using Medikit."',
37 | ]
38 |
39 | with patch("os.path.exists", return_value=True):
40 | commands = list()
41 | with patch("os.system", side_effect=commands.append) as os_system:
42 | feature.on_start(ProjectEvent(config=config))
43 | assert commands == []
44 |
45 | def test_on_end(self):
46 | feature, dispatcher = self.create_feature()
47 |
48 | commands = list()
49 | with patch("medikit.file.FileEvent") as fe, patch("os.system", side_effect=commands.append) as os_system:
50 | feature.on_end(ProjectEvent(config=self.create_config(), setup={"name": PACKAGE_NAME}))
51 |
52 | # TODO
53 | @pytest.mark.skip()
54 | def test_on_file_change(self):
55 | self.fail()
56 |
--------------------------------------------------------------------------------
/tests/feature/test_feature_pytest.py:
--------------------------------------------------------------------------------
1 | import medikit
2 | from medikit.events import ProjectEvent
3 | from medikit.feature.make import Makefile, MakefileEvent
4 | from medikit.feature.pytest import PytestFeature
5 | from medikit.testing import FeatureTestCase
6 |
7 | PACKAGE_NAME = "foo.bar"
8 |
9 |
10 | class TestPytestFeature(FeatureTestCase):
11 | feature_type = PytestFeature
12 | required_features = {"python", "make", "pytest"}
13 |
14 | def test_configure(self):
15 | feature, dispatcher = self.create_feature()
16 | listeners = dispatcher.get_listeners()
17 |
18 | assert feature.on_start in listeners[medikit.on_start]
19 | assert feature.on_make_generate in listeners["medikit.feature.make.on_generate"]
20 |
21 | def test_on_make_generate(self):
22 | pytest_feature, _ = self.create_feature()
23 |
24 | event = MakefileEvent(PACKAGE_NAME, Makefile(), self.create_config())
25 | pytest_feature.on_make_generate(event)
26 |
27 | assert {"test"} == set(dict(event.makefile.targets).keys())
28 | assert {"PYTEST", "PYTEST_OPTIONS"} == set(event.makefile.environ)
29 |
30 | def test_on_start(self):
31 | feature, _ = self.create_feature()
32 | event = ProjectEvent(config=self.create_config(), setup={"name": PACKAGE_NAME})
33 | feature.on_start(event)
34 |
35 | def test_set_version(self):
36 | # test without setting the version, should be the current default
37 | config = self.create_config()
38 | event = ProjectEvent(config=config, setup={"name": PACKAGE_NAME})
39 | feature, _ = self.create_feature()
40 | feature.on_python_generate(event)
41 | assert list(config["python"].get_requirements(extra="dev")) == [
42 | "coverage ~= 4.5",
43 | "pytest ~= 4.6",
44 | "pytest-cov ~= 2.7",
45 | ]
46 |
47 | # test with setting the version, should override the default
48 | config = self.create_config()
49 | config.require("pytest").set_version("~=5.0")
50 | event = ProjectEvent(config=config, setup={"name": PACKAGE_NAME})
51 | feature, _ = self.create_feature()
52 | feature.on_python_generate(event)
53 | assert list(config["python"].get_requirements(extra="dev")) == [
54 | "coverage ~= 4.5",
55 | "pytest ~= 5.0",
56 | "pytest-cov ~= 2.7",
57 | ]
58 |
--------------------------------------------------------------------------------
/tests/feature/test_feature_python.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import pytest
4 |
5 | import medikit
6 | from medikit.events import ProjectEvent
7 | from medikit.feature.make import MakeFeature, Makefile, MakefileEvent
8 | from medikit.feature.python import PythonConfig, PythonFeature
9 | from medikit.testing import FeatureTestCase
10 |
11 | PACKAGE_NAME = "foo.bar"
12 |
13 |
14 | class TestPythonConfig(TestCase):
15 | def create_python_config(self):
16 | return PythonConfig()
17 |
18 | def test_namespace_packages(self):
19 | config = self.create_python_config()
20 | config.setup(name=PACKAGE_NAME)
21 | init_files = list(config.get_init_files())
22 | assert init_files == [
23 | ("foo", "foo/__init__.py", {"is_namespace": True}),
24 | ("foo/bar", "foo/bar/__init__.py", {"is_namespace": False}),
25 | ]
26 |
27 |
28 | class TestPythonFeature(FeatureTestCase):
29 | feature_type = PythonFeature
30 |
31 | def create_config(self):
32 | config = super().create_config()
33 | config.require("make")
34 | python = config.require("python")
35 | python.setup(name=PACKAGE_NAME)
36 | return config
37 |
38 | def test_configure(self):
39 | feature, dispatcher = self.create_feature()
40 | listeners = dispatcher.get_listeners()
41 |
42 | assert feature.on_start in listeners[medikit.on_start]
43 | assert feature.on_make_generate in listeners["medikit.feature.make.on_generate"]
44 |
45 | def test_on_make_generate(self):
46 | python_feature, dispatcher = self.create_feature()
47 | config = self.create_config()
48 |
49 | python_feature.on_make_generate(MakefileEvent(PACKAGE_NAME, Makefile(), config))
50 |
51 | make_feature, dispatcher = self.create_feature(feature_type=MakeFeature)
52 | self.create_feature(dispatcher=dispatcher)
53 | make_feature.on_start(ProjectEvent(config=config, setup={"name": PACKAGE_NAME}))
54 |
55 | assert sorted(dict(make_feature.makefile.targets).keys()) == [
56 | "clean",
57 | "help",
58 | "install",
59 | "install-dev",
60 | "medikit",
61 | "quick",
62 | "update",
63 | "update-requirements",
64 | ]
65 |
66 | def test_on_start(self):
67 | config = self.create_config()
68 |
69 | feature, dispatcher = self.create_feature()
70 | event = ProjectEvent(config=config, setup={"name": PACKAGE_NAME, "python_requires": ">=3.5"})
71 | feature.on_start(event)
72 |
73 | assert event.setup["name"] == PACKAGE_NAME
74 | assert event.setup["python_requires"] == ">=3.5"
75 |
--------------------------------------------------------------------------------
/tests/test_event.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from unittest import TestCase
3 |
4 | import pytest
5 |
6 | from medikit.events import ProjectEvent
7 |
8 |
9 | class TestProjectEvent(TestCase):
10 | def _test_constructor(self, **kwargs):
11 | e = ProjectEvent(**kwargs)
12 |
13 | variables, files, setup = kwargs.get("variables", {}), kwargs.get("files", {}), kwargs.get("setup", {})
14 | assert isinstance(e.variables, OrderedDict)
15 | assert len(e.variables) == len(variables)
16 | assert isinstance(e.files, dict)
17 | assert len(e.files) == len(files)
18 | assert isinstance(e.setup, OrderedDict)
19 | assert len(e.setup) == len(setup)
20 |
21 | def test_basics(self):
22 | self._test_constructor(config=None)
23 | self._test_constructor(config=None, variables={}, files={}, setup={})
24 | self._test_constructor(config=None, variables={"foo": "bar"}, files={}, setup={"name": "my.pkg"})
25 |
26 | with pytest.raises(TypeError):
27 | self._test_constructor(unknown="foo")
28 |
--------------------------------------------------------------------------------
/tests/test_pipelines.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from medikit.steps import Step
4 |
5 |
6 | class FailingStep(Step):
7 | def run(self, meta):
8 | self.exec("false")
9 | self.set_complete()
10 |
11 |
12 | # Issue #64: A failing shell command execution should not be considered as a success.
13 | def test_failing_step():
14 | step = FailingStep()
15 | with pytest.raises(RuntimeError):
16 | step.run({})
17 | assert not step.complete
18 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from medikit.utils import is_identifier
2 |
3 |
4 | def test_is_identifier():
5 | assert is_identifier("") is False
6 | assert is_identifier("foo") is True
7 | assert is_identifier("foo_bar") is True
8 | assert is_identifier("foo bar") is False
9 |
--------------------------------------------------------------------------------