Welcome to scaraplate’s documentation!¶
Documentation: | https://scaraplate.readthedocs.io/ |
---|---|
Source Code: | https://github.com/rambler-digital-solutions/scaraplate |
Issue Tracker: | https://github.com/rambler-digital-solutions/scaraplate/issues |
PyPI: | https://pypi.org/project/scaraplate/ |
Introduction¶
Scaraplate is a wrapper around cookiecutter which allows to repeatedly rollup project templates onto concrete projects.
Cookiecutter is a great tool which allows to create projects from templates. However, it lacks ability to update the already created projects from updated templates. Scaraplate provides a solution to this problem.
To use scaraplate, you would have to add a scaraplate.yaml
file near
the cookiecutter.json
of your cookiecutter template.
Then, to rollup the changes from the updated template, you will need
to simply call this command (which can even be automated!):
scaraplate rollup ./path/to/template ./path/to/project --no-input
This allows to easily (and automatically) sync the projects from the template, greatly simplifying the unification of the projects’ structure.
CI pipelines, code linting settings, test runners, directory structures, artifacts building tend to greatly vary between the projects. Once described in the template which is easy to rollup onto the specific projects, projects unification becomes a trivial task. Everything can be defined in the template just once and then regularly be synced onto your projects, see Rollup Automation.
How it works¶
The scaraplate rollup
command does the following:
- Retrieve cookiecutter template variables from the previous rollup (see Scaraplate Template).
- Create a temporary dir, apply cookiecutter command with the retrieved variables to create a new temporary project.
- For each file in the temporary project, apply a strategy (see Strategies) which merges the file from the temporary project with the corresponding file in the target project.
Only the files which exist in the temporary project are touched by scaraplate in the target project.
The key component of scaraplate are the strategies.
Note that none of the strategies use git history or any git-like
merging. In fact, scaraplate strategies make no assumptions about
the code versioning system used by the target project.
Instead, the merging between the files is defined solely by strategies
which generate the output based on the two files and the settings in
the scaraplate.yaml
.
Scaraplate is quite extensible. Many parts are replaceable with custom implementations in Python.
Quickstart¶
scaraplate requires Python 3.7 or newer.
Installation:
pip install scaraplate
Scaraplate also requires git
to be installed in the system
(see Scaraplate Template).
To get started with scaraplate, you need to:
- Prepare a template (see Scaraplate Template and specifically Scaraplate Example Template).
- Roll it up on your projects.
Project Name¶

The project name is inspired by a bug which rolls up the brown balls of … well, stuff, (the template) everywhere (the projects).
scarab + template = scaraplate
Scaraplate Template¶
Scaraplate uses cookiecutter under the hood, so the scaraplate template is a cookiecutter template with the following properties:
- There must be a
scaraplate.yaml
config in the root of the template dir (near thecookiecutter.json
). - The template dir must be a git repo (because some strategies might render URLs to the template project and HEAD commit, making it easy to find out what template was used to rollup, see Template Git Remotes).
- The cookiecutter’s project dir must be called
project_dest
, i.e. the template must reside in the{{cookiecutter.project_dest}}
directory. - The template must contain a file which renders the current cookiecutter context. Scaraplate then reads that context to re-apply cookiecutter template on subsequent rollups (see Cookiecutter Context Types).
scaraplate.yaml
contains:
- strategies (see Strategies),
- cookiecutter context type (see Cookiecutter Context Types),
- template git remote (see Template Git Remotes).
Note
Neither scaraplate.yaml
nor cookiecutter.json
would get
to the target project. These two files exist only in the template
repo. The files that would get to the target project are located
in the inner {{cookiecutter.project_dest}}
directory of the template repo.
scaraplate rollup
has a --no-input
switch which doesn’t ask for
cookiecutter context values. This can be used to automate rollups
when the cookiecutter context is already present in the target project
(i.e. scaraplate rollup
has been applied before). But the first rollup
should be done without the --no-input
option, so the cookiecutter
context values could be filled by hand interactively.
The arguments to the scaraplate rollup
command must be local
directories (i.e. the template git repo must be cloned manually,
scaraplate doesn’t support retrieving templates from git remote directly).
Scaraplate Example Template¶
We maintain an example template for a new Python project here: https://github.com/rambler-digital-solutions/scaraplate-example-template
You may use it as a starting point for creating your own scaraplate template. Of course it doesn’t have to be for a Python project: the cookiecutter template might be for anything. A Python project is just an example.
Creating a new project from the template¶
$ git clone https://github.com/rambler-digital-solutions/scaraplate-example-template.git
$ scaraplate rollup ./scaraplate-example-template ./myproject
`myproject1/.scaraplate.conf` file doesn't exist, continuing with an empty context...
`project_dest` must equal to "myproject"
project_dest [myproject]:
project_monorepo_name []:
python_package [myproject]:
metadata_name [myproject]:
metadata_author: Kostya Esmukov
metadata_author_email: kostya@esmukov.net
metadata_description: My example project
metadata_long_description [file: README.md]:
metadata_url [https://github.com/rambler-digital-solutions/myproject]:
coverage_fail_under [100]: 90
mypy_enabled [1]:
Done!
$ tree -a myproject
myproject
├── .editorconfig
├── .gitignore
├── .scaraplate.conf
├── MANIFEST.in
├── Makefile
├── README.md
├── mypy.ini
├── setup.cfg
├── setup.py
├── src
│ └── myproject
│ └── __init__.py
└── tests
├── __init__.py
└── test_metadata.py
3 directories, 12 files
The example template also contains a project_monorepo_name
variable
which simplifies creating subprojects in monorepos (e.g. a single git
repository for multiple projects). In this case scaraplate should be
applied to the inner projects:
$ scaraplate rollup ./scaraplate-example-template ./mymonorepo/innerproject
`mymonorepo/innerproject/.scaraplate.conf` file doesn't exist, continuing with an empty context...
`project_dest` must equal to "innerproject"
project_dest [innerproject]:
project_monorepo_name []: mymonorepo
python_package [mymonorepo_innerproject]:
metadata_name [mymonorepo-innerproject]:
metadata_author: Kostya Esmukov
metadata_author_email: kostya@esmukov.net
metadata_description: My example project in a monorepo
metadata_long_description [file: README.md]:
metadata_url [https://github.com/rambler-digital-solutions/mymonorepo]:
coverage_fail_under [100]: 90
mypy_enabled [1]:
Done!
$ tree -a mymonorepo
mymonorepo
└── innerproject
├── .editorconfig
├── .gitignore
├── .scaraplate.conf
├── MANIFEST.in
├── Makefile
├── README.md
├── mypy.ini
├── setup.cfg
├── setup.py
├── src
│ └── mymonorepo_innerproject
│ └── __init__.py
└── tests
├── __init__.py
└── test_metadata.py
4 directories, 12 files
Updating a project from the template¶
$ scaraplate rollup ./scaraplate-example-template ./myproject --no-input
Continuing with the following context from the `myproject/.scaraplate.conf` file:
{'_template': 'scaraplate-example-template',
'coverage_fail_under': '90',
'metadata_author': 'Kostya Esmukov',
'metadata_author_email': 'kostya@esmukov.net',
'metadata_description': 'My example project',
'metadata_long_description': 'file: README.md',
'metadata_name': 'myproject',
'metadata_url': 'https://github.com/rambler-digital-solutions/myproject',
'mypy_enabled': '1',
'project_dest': 'myproject',
'project_monorepo_name': '',
'python_package': 'myproject'}
Done!
Cookiecutter Context Types¶
cookiecutter context are the variables specified in cookiecutter.json
,
which should be provided to cookiecutter to cut a project from the template.
The context should be generated by one of the files in the template, so scaraplate could read these variables and rollup the template automatically (i.e. without asking for these variables).
The default context reader is ScaraplateConf
, but a custom one
might be specified in scaraplate.yaml
like this:
cookiecutter_context_type: scaraplate.cookiecutter.SetupCfg
Bases:
abc.ABC
The abstract base class for retrieving cookiecutter context from the target project.
This class can be extended to provide a custom implementation of the context reader.
Init the context reader.
Retrieve the context.
If the target file doesn’t exist,
FileNotFoundError
must be raised.If the file doesn’t contain the context, an empty dict should be returned.
Built-in Cookiecutter Context Types¶
Bases:
scaraplate.cookiecutter.CookieCutterContext
A default context reader which assumes that the cookiecutter template contains the following file named
.scaraplate.conf
in the root of the project dir:[cookiecutter_context] {%- for key, value in cookiecutter.items()|sort %} {%- if key not in ('_output_dir',) %} {{ key }} = {{ value }} {%- endif %} {%- endfor %}
Cookiecutter context would be rendered in the target project by this file, and this class is able to retrieve that context from it.
Bases:
scaraplate.cookiecutter.CookieCutterContext
A context reader which retrieves the cookiecutter context from a section in
setup.cfg
file.The
setup.cfg
file must be in the cookiecutter template and must contain the following section:[tool:cookiecutter_context] {%- for key, value in cookiecutter.items()|sort %} {%- if key not in ('_output_dir',) %} {{ key }} = {{ value }} {%- endif %} {%- endfor %}
Template Maintenance¶
Given that scaraplate provides ability to update the already created projects from the updated templates, it’s worth discussing the maintenance of a scaraplate template.
Removing a template variable¶
Template variables could be used as feature flags to gradually introduce some changes in the templates which some target projects might not use (yet) by disabling the flag.
But once the migration is complete, you might want to remove the no longer needed variable.
Fortunately this is very simple: just stop using it in the template and
remove it from cookiecutter.json
. On the next scaraplate rollup
the removed variable will be automatically removed from
the cookiecutter context file.
Adding a new template variable¶
The process for adding a new variable is the same as for removing one:
just add it to the cookiecutter.json
and you can start using it in
the template.
If the next scaraplate rollup
is run with --no-input
, the new
variable will have the default value as specified in cookiecutter.json
.
If you need a different value, you have 2 options:
- Run
scraplate rollup
without the--no-input
flag so the value for the new variable could be asked interactively. - Manually add the value to
the cookiecutter context section
so the next
rollup
could pick it up.
Restructuring files¶
Scaraplate strategies intentionally don’t provide support for anything more complex than a simple file-to-file change. It means that a scaraplate template cannot:
- Delete or move files in the target project;
- Take multiple files and union them.
The reason is simple: such operations are always the one-time ones so it is just easier to perform them manually once than to maintain that logic in the template.
Patterns¶
This section contains some patterns which might be helpful for creating and maintaining a scaraplate template.
Feature flags¶
Let’s say you have a template which you have applied to dozens of your projects.
And now you want to start gradually introducing a new feature, let it be a new linter.
You probably would not want to start using the new thing everywhere at once. Instead, usually you start with one or two projects, gain experience and then start rolling it up on the other projects.
For that you can use template variables as feature flags.
The example template contains
a mypy_enabled
variable which demonstrates this concept. Basically
it is a regular cookiecutter variable, which can take different values
in the target projects and thus affect the template by enabling or disabling
the new feature.
Include files¶
Consider Makefile
. On one hand, you would definitely want to have some
make targets to come from the template; on the other hand, you might need
to introduce custom make targets in some projects. Coming up with a scaraplate
strategy which could merge such a file would be quite difficult.
Fortunately, Makefile
allows to include other files. So the solution
is quite trivial: have Makefile
synced from the template (with
the scaraplate.strategies.Overwrite
strategy), and include
a Makefile.inc
file from there which will not be overwritten by the template.
This concept is demonstrated in the example template.
Manual merging¶
Sometimes you need to merge some files which might be modified in the target
projects and for which there’s no suitable strategy yet. In this case
you can use scaraplate.strategies.TemplateHash
strategy as
a temporary solution: it would overwrite the file each time a new
git commit is added to the template, but keep the file unchanged since
the last rollup of the same template commit.
The example template uses this
approach for setup.py
.
Create files conditionally¶
Cookiecutter hooks can be used to post-process the generated temporary project. For example, you might want to skip some files from the template depending on the variables.
The example template contains
an example hook which deletes mypy.ini
file when the mypy_enabled
variable is not set to 1
.
Strategies¶
Strategies do the merging between the files from template and the target.
Strategies are specified in the scaraplate.yaml
file located in the root
of the template dir.
Sample scaraplate.yaml
excerpt:
default_strategy: scaraplate.strategies.Overwrite
strategies_mapping:
setup.py: scaraplate.strategies.TemplateHash
src/*/__init__.py: scaraplate.strategies.IfMissing
package.json:
strategy: mypackage.mymodule.MyPackageJson
config:
my_key: True
"src/{{ cookiecutter.myvariable }}.md": scaraplate.strategies.IfMissing
The strategy should be an importable Python class which implements
Strategy
.
default_strategy
and strategies_mapping
keys are the required ones.
The strategy value might be either a string (specifying a Python class),
or a dict of two keys – strategy
and config
. The first form
is just a shortcut for specifying a strategy with an empty config.
config
would be passed to the Strategy’s __init__
which would
be validated with the inner Schema
class.
-
class
scaraplate.strategies.
Strategy
¶ Bases:
abc.ABC
The abstract base class for a scaraplate Strategy.
To implement and use a custom strategy, the following needs to be done:
- Create a new Python class which implements
Strategy
- Override the inner
Schema
class if you need to configure your strategy fromscaraplate.yaml
. - Implement the
apply
method.
Assuming that the new strategy class is importable in the Python environment in which scaraplate is run, to use the strategy you need to specify it in
scaraplate.yaml
, e.g.strategies_mapping: myfile.txt: mypackage.mymodule.MyStrategy
-
class
Schema
¶ An empty default schema which doesn’t accept any parameters.
-
__init__
(*, target_contents: Optional[BinaryIO], template_contents: BinaryIO, template_meta: scaraplate.template.TemplateMeta, config: Dict[str, Any]) → None¶ Init the strategy.
Parameters: - target_contents – The file contents in the target project.
None
if the file doesn’t exist. - template_contents – The file contents from the template (after cookiecutter is applied).
- template_meta – Template metadata,
see
scaraplate.template.TemplateMeta
. - config – The strategy config from
scaraplate.yaml
. It is validated in this__init__
with the innerSchema
class.
- target_contents – The file contents in the target project.
-
apply
() → BinaryIO¶ Apply the Strategy.
Returns: The resulting file contents which would overwrite the target file.
- Create a new Python class which implements
Built-in Strategies¶
-
class
scaraplate.strategies.
Overwrite
¶ Bases:
scaraplate.strategies.Strategy
A simple strategy which always overwrites the target files with the ones from the template.
-
class
Schema
¶ An empty default schema which doesn’t accept any parameters.
-
class
-
class
scaraplate.strategies.
IfMissing
¶ Bases:
scaraplate.strategies.Strategy
A strategy which writes the file from the template only if it doesn’t exist in target.
-
class
Schema
¶ An empty default schema which doesn’t accept any parameters.
-
class
-
class
scaraplate.strategies.
SortedUniqueLines
¶ Bases:
scaraplate.strategies.Strategy
A strategy which combines both template and target files, sorts the combined lines and keeps only unique ones.
However, the comments in the beginning of the files are treated differently. They would be stripped from the target and replaced with the ones from the template. The most common usecase for this are the License headers.
Sample
scaraplate.yaml
excerpt:strategies_mapping: MANIFEST.in: strategy: scaraplate.strategies.SortedUniqueLines .gitignore: strategy: scaraplate.strategies.SortedUniqueLines
-
class
Schema
¶ Allowed params:
comment_pattern
[^ *([;#%]|//)
] – a PCRE pattern which should match the line with a comment.
-
class
-
class
scaraplate.strategies.
TemplateHash
¶ Bases:
scaraplate.strategies.Strategy
A strategy which appends to the target file a git commit hash of the template being applied; and the subsequent applications of the same template for this file are ignored until the HEAD commit of the template changes.
This strategy is useful when a file needs to be different from the template but there’s no suitable automated strategy yet, so it should be manually resynced on template updates.
This strategy overwrites the target file on each new commit in the template. There’s also a
RenderedTemplateFileHash
strategy which does it less frequently: only when the source file from the template has changes.Sample
scaraplate.yaml
excerpt:strategies_mapping: setup.py: strategy: scaraplate.strategies.TemplateHash config: line_comment_start: '#' max_line_length: 87 max_line_linter_ignore_mark: ' # noqa' Jenkinsfile: strategy: scaraplate.strategies.TemplateHash config: line_comment_start: '//'
This would result in the following:
setup.py:
...file contents... # Generated by https://github.com/rambler-digital-solutions/scaraplate # From https://github.com/rambler-digital-solutions/scaraplate-example-template/commit/1111111111111111111111111111111111111111 # noqa
Jenkinsfile:
...file contents... // Generated by https://github.com/rambler-digital-solutions/scaraplate // From https://github.com/rambler-digital-solutions/scaraplate-example-template/commit/1111111111111111111111111111111111111111
-
class
Schema
¶ Allowed params:
line_comment_start
[#
] – The prefix which should be used to start a line comment.max_line_length
[None] – The maximum line length for the appended line comments after which themax_line_linter_ignore_mark
suffix should be added.max_line_linter_ignore_mark
[# noqa
] – The linter’s line ignore mark for the appended line comments which are longer thanmax_line_length
columns. The default# noqa
mark silencesflake8
.
-
class
-
class
scaraplate.strategies.
RenderedTemplateFileHash
¶ Bases:
scaraplate.strategies.TemplateHash
A strategy which appends to the target file a hash of the rendered template file; and the subsequent applications of the same template for this file are ignored until the rendered file has changes.
This strategy is similar to
TemplateHash
with the difference that the target file is rewritten less frequently: only when the hash of the source file from the template is changed.New in version 0.2.
Sample
scaraplate.yaml
excerpt:strategies_mapping: setup.py: strategy: scaraplate.strategies.RenderedTemplateFileHash config: line_comment_start: '#' max_line_length: 87 max_line_linter_ignore_mark: ' # noqa' Jenkinsfile: strategy: scaraplate.strategies.RenderedTemplateFileHash config: line_comment_start: '//'
This would result in the following:
setup.py:
...file contents... # Generated by https://github.com/rambler-digital-solutions/scaraplate # RenderedTemplateFileHash d2671228e3dfc3e663bfaf9b5b151ce8 # From https://github.com/rambler-digital-solutions/scaraplate-example-template/commit/1111111111111111111111111111111111111111 # noqa
Jenkinsfile:
...file contents... // Generated by https://github.com/rambler-digital-solutions/scaraplate // RenderedTemplateFileHash d2as1228eb7233e663bfaf9b5b151ce8 // From https://github.com/rambler-digital-solutions/scaraplate-example-template/commit/1111111111111111111111111111111111111111
-
class
Schema
¶ Allowed params:
line_comment_start
[#
] – The prefix which should be used to start a line comment.max_line_length
[None] – The maximum line length for the appended line comments after which themax_line_linter_ignore_mark
suffix should be added.max_line_linter_ignore_mark
[# noqa
] – The linter’s line ignore mark for the appended line comments which are longer thanmax_line_length
columns. The default# noqa
mark silencesflake8
.
-
class
-
class
scaraplate.strategies.
ConfigParserMerge
¶ Bases:
scaraplate.strategies.Strategy
A strategy which merges INI-like files (with
configparser
).The resulting file is the one from the template with the following modifications:
- Comments are stripped
- INI file is reformatted (whitespaces are cleaned, sections and keys are sorted)
- Sections specified in the
preserve_sections
config list are preserved from the target file. - Keys specified in the
preserve_keys
config list are preserved from the target file.
This strategy cannot be used to merge config files which contain keys without a preceding section declaration (e.g.
.editorconfig
won’t work).Sample
scaraplate.yaml
excerpt:strategies_mapping: .pylintrc: strategy: scaraplate.strategies.ConfigParserMerge config: preserve_sections: [] preserve_keys: - sections: ^MASTER$ keys: ^extension-pkg-whitelist$ - sections: ^TYPECHECK$ keys: ^ignored- tox.ini: strategy: scaraplate.strategies.ConfigParserMerge config: preserve_sections: - sections: ^tox$ preserve_keys: - sections: ^testenv keys: ^extras$ pytest.ini: strategy: scaraplate.strategies.ConfigParserMerge config: preserve_sections: [] preserve_keys: - sections: ^pytest$ keys: ^python_files$ .isort.cfg: strategy: scaraplate.strategies.ConfigParserMerge config: preserve_sections: [] preserve_keys: - sections: ^settings$ keys: ^known_third_party$
-
class
Schema
¶ Allowed params:
preserve_keys
(required) – the list of config keys which should be preserved from the target file. Values schema:sections
(required) – a PCRE pattern matching sections containing the keys to preserve.keys
(required) – a PCRE pattern matching keys in the matched sections.
preserve_sections
(required) – the list of config sections which should be preserved from the target file. If the matching section exists in the template, it would be fully overwritten. Values schema:sections
(required) – a PCRE pattern matching sections which should be preserved from the target.excluded_keys
[None] – a PCRE pattern matching the keys which should not be overwritten in the template when preserving the section.
-
class
scaraplate.strategies.
SetupCfgMerge
¶ Bases:
scaraplate.strategies.ConfigParserMerge
A strategy which merges the Python’s
setup.cfg
file.Based on the
ConfigParserMerge
strategy, additionally containing amerge_requirements
config option for merging the lists of Python requirements between the files.Sample
scaraplate.yaml
excerpt:strategies_mapping: setup.cfg: strategy: scaraplate.strategies.SetupCfgMerge config: merge_requirements: - sections: ^options$ keys: ^install_requires$ - sections: ^options\.extras_require$ keys: ^develop$ preserve_keys: - sections: ^tool:pytest$ keys: ^testpaths$ - sections: ^build$ keys: ^executable$ preserve_sections: - sections: ^mypy- - sections: ^options\.data_files$ - sections: ^options\.entry_points$ - sections: ^options\.extras_require$
-
class
Schema
¶ Allowed params:
preserve_keys
(required) – the list of config keys which should be preserved from the target file. Values schema:sections
(required) – a PCRE pattern matching sections containing the keys to preserve.keys
(required) – a PCRE pattern matching keys in the matched sections.
preserve_sections
(required) – the list of config sections which should be preserved from the target file. If the matching section exists in the template, it would be fully overwritten. Values schema:sections
(required) – a PCRE pattern matching sections which should be preserved from the target.excluded_keys
[None] – a PCRE pattern matching the keys which should not be overwritten in the template when preserving the section.
merge_requirements
(required) – the list of config keys containing the lists of Python requirements which should be merged together. Values schema:sections
(required) – a PCRE pattern matching sections containing the keys with requirements.keys
(required) – a PCRE pattern matching keys in the matched sections.
-
class
Template Git Remotes¶
Scaraplate assumes that the template dir is a git repo.
Strategies receive a scaraplate.template.TemplateMeta
instance
which contains URLs to the template’s project and the HEAD git commit
on a git remote’s web interface (such as GitHub). These URLs might be
rendered in the target files by the strategies.
Scaraplate has built-in support for some popular git remotes. The remote is attempted to be detected automatically, but if that fails, it should be specified manually.
Sample scaraplate.yaml
excerpt:
git_remote_type: scaraplate.gitremotes.GitHub
-
class
scaraplate.template.
TemplateMeta
¶ Bases:
tuple
Metadata of the template’s git repo status.
-
commit_hash
¶ Alias for field number 1
-
commit_url
¶ Alias for field number 2
-
git_project_url
¶ Alias for field number 0
-
head_ref
¶ Alias for field number 4
-
is_git_dirty
¶ Alias for field number 3
-
-
class
scaraplate.gitremotes.
GitRemote
(remote: str)¶ Bases:
abc.ABC
Base class for a git remote implementation, which generates http URLs from a git remote (either ssh of http) and a commit hash.
-
__init__
(remote: str) → None¶ Init the git remote.
Parameters: remote – A git remote, either ssh or http(s).
-
commit_url
(commit_hash: str) → str¶ Return a commit URL at the given git remote.
Parameters: commit_hash – Git commit hash.
-
project_url
() → str¶ Return a project URL at the given git remote.
-
Built-in Git Remotes¶
-
class
scaraplate.gitremotes.
GitLab
(remote: str)¶ Bases:
scaraplate.gitremotes.GitRemote
GitLab git remote implementation.
-
class
scaraplate.gitremotes.
GitHub
(remote: str)¶ Bases:
scaraplate.gitremotes.GitRemote
GitHub git remote implementation.
-
class
scaraplate.gitremotes.
BitBucket
(remote: str)¶ Bases:
scaraplate.gitremotes.GitRemote
BitBucket git remote implementation.
Rollup Automation¶
Once you get comfortable with manual rollups, you might want to set up regularly executed automated rollups.
At this moment scaraplate doesn’t provide a CLI for that, but there’s a quite extensible Python code which simplifies implementation of custom scenarios.
Supported automation scenarios¶
GitLab Merge Request¶
GitLab integration requires python-gitlab package, which can be installed with:
pip install 'scaraplate[gitlab]'
Sample rollup.py
script:
from scaraplate import automatic_rollup
from scaraplate.automation.gitlab import (
GitLabCloneTemplateVCS,
GitLabMRProjectVCS,
)
automatic_rollup(
template_vcs_ctx=GitLabCloneTemplateVCS.clone(
project_url="https://mygitlab.example.org/myorg/mytemplate",
private_token="your_access_token",
clone_ref="master",
),
project_vcs_ctx=GitLabMRProjectVCS.clone(
gitlab_url="https://mygitlab.example.org",
full_project_name="myorg/mytargetproject",
private_token="your_access_token",
changes_branch="scheduled-template-update",
clone_ref="master",
),
)
This script would do the following:
git clone
the template repo to a tempdir;git clone
the project repo to a tempdir;- Run
scaraplate rollup ... --no-input
; - Would do nothing if rollup didn’t change anything; otherwise it would
create a commit with the changes, push it to the
scheduled-template-update
branch and open a GitLab Merge Request from this branch.
If a MR already exists, GitLabMRProjectVCS
does the following:
- A one-commit git diff is compared between the already existing MR’s branch and the locally committed branch (in a tempdir). If diffs are equal, nothing is done.
- If diffs are different, the existing MR’s branch is removed from the remote, effectively closing the old MR, and a new branch is pushed, which is followed by creation of a new MR.
To have this script run daily, crontab can be used. Assuming that the script
is located at /opt/rollup.py
and the desired time for execution is 9:00,
it might look like this:
$ crontab -e
# Add the following line:
00 9 * * * python3 /opt/rollup.py
Git push¶
GitLabCloneTemplateVCS
and
GitLabMRProjectVCS
are based off
GitCloneTemplateVCS
and
GitCloneProjectVCS
correspondingly. GitLab classes add GitLab-specific git-clone URL
generation and Merge Request creation. The rest (git clone, commit, push)
is done in the GitCloneTemplateVCS
and GitCloneProjectVCS
classes.
GitCloneTemplateVCS
and
GitCloneProjectVCS
classes work
with any git remote. If you’re okay with just pushing a branch with
updates (without opening a Merge Request/Pull Request), then you can
use the following:
Sample rollup.py
script:
from scaraplate import automatic_rollup, GitCloneProjectVCS, GitCloneTemplateVCS
automatic_rollup(
template_vcs_ctx=GitCloneTemplateVCS.clone(
clone_url="https://github.com/rambler-digital-solutions/scaraplate-example-template.git",
clone_ref="master",
),
project_vcs_ctx=GitCloneProjectVCS.clone(
clone_url="https://mygit.example.org/myrepo.git",
clone_ref="master",
changes_branch="scheduled-template-update",
commit_author="scaraplate <yourorg@yourcompany>",
),
)
Python API¶
-
scaraplate.automation.base.
automatic_rollup
(*, template_vcs_ctx: AbstractContextManager[TemplateVCS], project_vcs_ctx: AbstractContextManager[ProjectVCS], extra_context: Optional[Mapping[str, str]] = None) → None¶ The main function of the automated rollup implementation.
This function accepts two context managers, which should return two classes:
TemplateVCS
andProjectVCS
, which represent the cloned template and target project correspondingly.The context managers should prepare the repos, e.g. they should create a temporary directory, clone a repo there, and produce a
TemplateVCS
orProjectVCS
class instance.This function then applies scaraplate rollup of the template to the target project in no-input mode. If the target project contains any changes (as reported by
ProjectVCS.is_dirty()
), they will be committed by callingProjectVCS.commit_changes()
.New in version 0.2.
-
class
scaraplate.automation.base.
TemplateVCS
¶ Bases:
abc.ABC
A base class representing a template retrieved from a VCS (probably residing in a temporary directory).
The resulting directory with template must be within a git repository, see Scaraplate Template for details. But it doesn’t mean that it must be retrieved from git. Template might be retrieved from anywhere, it just has to be in git at the end. That git repo will be used to fill the
TemplateMeta
structure.-
dest_path
¶ Path to the root directory of the template.
-
template_meta
¶ TemplateMeta
filled using the template’s git repo.
-
-
class
scaraplate.automation.base.
ProjectVCS
¶ Bases:
abc.ABC
A base class representing a project retrieved from a VCS (probably residing in a temporary directory).
The project might use any VCS, at this point there’re no assumptions made by scaraplate about the VCS.
-
commit_changes
(template_meta: scaraplate.template.TemplateMeta) → None¶ Commit the changes made to the project. This method is responsible for delivering the changes back to the place the project was retrieved from. For example, if the project is using
git
and it was cloned to a temporary directory, then this method should commit the changes and push them back to git remote.This method will be called only if
ProjectVCS.is_dirty()
has returned True.
-
dest_path
¶ Path to the root directory of the project.
-
is_dirty
() → bool¶ Tell whether the project has any changes not committed to the VCS.
-
-
class
scaraplate.automation.git.
GitCloneTemplateVCS
(template_path: pathlib.Path, template_meta: scaraplate.template.TemplateMeta)¶ Bases:
scaraplate.automation.base.TemplateVCS
A ready to use
TemplateVCS
implementation which:- Uses git
- Clones a git repo with the template to a temporary directory (which is cleaned up afterwards)
- Allows to specify an inner dir inside the git repo as the template root (which is useful for monorepos)
-
classmethod
clone
(clone_url: str, *, clone_ref: Optional[str] = None, monorepo_inner_path: Optional[pathlib.Path] = None) → Iterator[scaraplate.automation.git.GitCloneTemplateVCS]¶ Provides an instance of this class by issuing
git clone
to a tempdir when entering the context manager. Returns a context manager object which after__enter__
returns an instance of this class.Parameters: - clone_url – Any valid
git clone
url. - clone_ref – Git ref to checkout after clone (i.e. branch or tag name).
- monorepo_inner_path – Path to the root dir of template
relative to the root of the repo. If
None
, the root of the repo will be used as the root of template.
- clone_url – Any valid
-
class
scaraplate.automation.git.
GitCloneProjectVCS
(project_path: pathlib.Path, git: scaraplate.automation.git.Git, *, changes_branch: str, commit_author: str, commit_message_template: str)¶ Bases:
scaraplate.automation.base.ProjectVCS
A ready to use
ProjectVCS
implementation which:- Uses git
- Clones a git repo with the project to a temporary directory (which is cleaned up afterwards)
- Allows to specify an inner dir inside the git repo as the project root (which is useful for monorepos)
- Implements
ProjectVCS.commit_changes()
asgit commit
+git push
.
-
classmethod
clone
(clone_url: str, *, clone_ref: Optional[str] = None, monorepo_inner_path: Optional[pathlib.Path] = None, changes_branch: str, commit_author: str, commit_message_template: str = 'Scheduled template update ({update_time:%Y-%m-%d})\n\n* scaraplate version: {scaraplate_version}\n* template commit: {template_meta.commit_url}\n* template ref: {template_meta.head_ref}\n') → Iterator[scaraplate.automation.git.GitCloneProjectVCS]¶ Provides an instance of this class by issuing
git clone
to a tempdir when entering the context manager. Returns a context manager object which after__enter__
returns an instance of this class.Parameters: - clone_url – Any valid
git clone
url. - clone_ref – Git ref to checkout after clone (i.e. branch or tag name).
- monorepo_inner_path – Path to the root dir of project
relative to the root of the repo. If
None
, the root of the repo will be used as the root of project. - changes_branch – The branch name where the changes should be
pushed in the remote. Might be the same as
clone_ref
. Note that this branch is never force-pushed. If upon push the branch already exists in remote and its one-commit diff is different from the one-commit diff of the just created local branch, then the remote branch will be deleted and the local branch will be pushed to replace the previous one. - commit_author – Author name to use for
git commit
, e.g.John Doe <john@example.org>
. - commit_message_template –
str.format()
template which is used to produce a commit message when committing the changes. Available format variables are:update_time
[datetime.datetime
] – the time of updatescaraplate_version
[str
] – scaraplate package versiontemplate_meta
[TemplateMeta
] – template meta returned byTemplateVCS.template_meta()
- clone_url – Any valid
-
class
scaraplate.automation.gitlab.
GitLabCloneTemplateVCS
(git_clone: scaraplate.automation.git.GitCloneTemplateVCS)¶ Bases:
scaraplate.automation.base.TemplateVCS
A class which extends
GitCloneTemplateVCS
with GitLab-specificclone_url
generation.-
classmethod
clone
(project_url: str, private_token: Optional[str] = None, *, clone_ref: Optional[str] = None, monorepo_inner_path: Optional[pathlib.Path] = None) → Iterator[scaraplate.automation.gitlab.GitLabCloneTemplateVCS]¶ Same as
GitCloneTemplateVCS.clone()
except thatclone_url
is replaced withproject_url
andprivate_token
.The
private_token
allows to clone private repos, which are visible only for an authenticated user.Parameters: - project_url – A URL to a GitLab project, e.g.
https://gitlab.example.org/myorganization/myproject
. - private_token – GitLab access token, see https://docs.gitlab.com/ce/api/#oauth2-tokens.
- project_url – A URL to a GitLab project, e.g.
-
classmethod
-
class
scaraplate.automation.gitlab.
GitLabMRProjectVCS
(git_clone: scaraplate.automation.git.GitCloneProjectVCS, *, gitlab_project, mr_title_template: str, mr_description_markdown_template: str)¶ Bases:
scaraplate.automation.base.ProjectVCS
A class which extends
GitCloneProjectVCS
with GitLab-specificclone_url
generation and opens a GitLab Merge Request aftergit push
.-
classmethod
clone
(gitlab_url: str, full_project_name: str, private_token: str, *, mr_title_template: str = 'Scheduled template update ({update_time:%Y-%m-%d})', mr_description_markdown_template: str = '* scaraplate version: `{scaraplate_version}`\n* template commit: {template_meta.commit_url}\n* template ref: {template_meta.head_ref}\n', commit_author: Optional[str] = None, **kwargs) → Iterator[scaraplate.automation.gitlab.GitLabMRProjectVCS]¶ Same as
GitCloneProjectVCS.clone()
with the following exceptions:clone_url
is replaced withgitlab_url
,full_project_name
andprivate_token
.- A GitLab Merge Request (MR) is opened after a successful
git push
.
The
private_token
allows to clone private repos, which are visible only for an authenticated user.As in
GitCloneProjectVCS.clone()
, thechanges_branch
might be the same asclone_ref
. In this case no MR will be opened.A MR will be created only if there’re any changes produced by scaraplate rollup. If a
changes_branch
is already present in remote (i.e. there is a previous automatic rollup which wasn’t merged yet), there’re two possibilities:- If one-commit diffs between the remote’s
changes_branch
and the localchanges_branch
are the same, nothing is done. It means that a MR already exists and it has the same patch as the one which was just produced locally. - If the diffs are different, the remote branch will be deleted, effectively closing the old MR, and a new one will be pushed instead, and a new MR will be opened.
The opened MRs are expected to be merged manually.
Parameters: - gitlab_url – A URL to the GitLab instance, e.g.
https://gitlab.example.org
. - full_project_name – Project name within gitlab, e.g.
myorganization/myproject
. - private_token –
GitLab access token, see https://docs.gitlab.com/ce/api/#oauth2-tokens.
- mr_title_template –
str.format()
template which is used to produce a MR title. Available format variables are:update_time
[datetime.datetime
] – the time of updatetemplate_meta
[TemplateMeta
] – template meta returned byTemplateVCS.template_meta()
- mr_description_markdown_template –
str.format()
template which is used to produce a MR description (which will be rendered as markdown). Available format variables are:update_time
[datetime.datetime
] – the time of updatescaraplate_version
[str
] – scaraplate package versiontemplate_meta
[TemplateMeta
] – template meta returned byTemplateVCS.template_meta()
- commit_author – Author name to use for
git commit
, e.g.John Doe <john@example.org>
. IfNone
, will be retrieved from GitLab as the name of the currently authenticated user (usingprivate_token
).
-
classmethod