Skip to content

Commit 844244a

Browse files
committed
Add first custom click types
* add `ansible`, `coding` and `net` custom click types * add `setup.py` for distribution * add `requirements-dev.txt` for development
1 parent 0e96976 commit 844244a

File tree

6 files changed

+262
-0
lines changed

6 files changed

+262
-0
lines changed

click_types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Python package that provides custom click types"""

click_types/ansible.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Module for custom click types regarding to ansible"""
2+
3+
import click
4+
import os
5+
import yaml
6+
7+
from ansible.errors import AnsibleError
8+
from ansible_vault import Vault
9+
from yaml import SafeLoader
10+
11+
12+
class AnsibleVaultParamType(click.ParamType):
13+
"""Provide a custom click type for ansible vaults.
14+
15+
This custom click type provides managing passed values in a given vault.
16+
* decrypt vault
17+
* save passed value
18+
* encrypt vault
19+
"""
20+
name = 'vault'
21+
22+
vault = str
23+
secret = str
24+
path = str
25+
26+
def __init__(self, vault: str, secret: str, path: str):
27+
"""Create a AnsibleVaultParamType object.
28+
29+
This method takes three arguments that are necessary to initialize a AnsibleVaultParamType object.
30+
31+
:param vault: Path to the vault file.
32+
:type vault: str
33+
:param secret: Name of the Environement variable that stores the vault passphrase.
34+
:type secret: str
35+
:param path: Path where the passed value should be saved in dotted notation.
36+
:type path: str
37+
"""
38+
try:
39+
s = os.getenv(secret)
40+
self.v = Vault(s)
41+
except (TypeError, ValueError):
42+
self.fail('Environment variable \'{0}\' not set.'.format(secret))
43+
44+
self.vault = vault
45+
self.path = path
46+
47+
super(AnsibleVaultParamType, self).__init__()
48+
49+
def convert(self, value, param, ctx):
50+
"""Open vault and save vaule at the given path.
51+
52+
:param value: the value passed
53+
:type value: str
54+
:param param: the parameter that we declared
55+
:type param: str
56+
:param ctx: context of the command
57+
:type ctx: str
58+
:return: the passed value as a checked semver
59+
:rtype: str
60+
"""
61+
data = {}
62+
try:
63+
if os.path.exists(self.vault):
64+
data = self.v.load(open(self.vault).read())
65+
except AnsibleError as e:
66+
if 'not vault encrypted data' in str(e):
67+
data = yaml.load(open(self.vault).read(), SafeLoader) or {}
68+
except Exception as e:
69+
self.fail('Decryption failed: {0}'.format(str(e)), param, ctx)
70+
71+
data = self._populate_data(data, self.path.split('.'), value)
72+
with open(self.vault, "w") as f:
73+
yaml.dump(data, f)
74+
75+
try:
76+
self.v.dump(data, open(self.vault, 'w'))
77+
except: # noqa: E722
78+
self.fail('Error while encrypting data', param, ctx)
79+
80+
return self.path
81+
82+
def _populate_data(self, input={}, keys=[], value=None):
83+
"""Save value at the desired position in vault.
84+
85+
This method takes vault data, a list of keys where to store the value.
86+
87+
:param input: The dictionary of vault data, defaults to {}
88+
:type input: dict, optional
89+
:param keys: List of keys that describe the desired position in vault, defaults to []
90+
:type keys: list, optional
91+
:param value: The value to store in vault, defaults to None
92+
:type value: str, optional
93+
:return: The vault data extended by `value` at the desired position.
94+
:rtype: dict
95+
"""
96+
data = input.copy()
97+
98+
if keys:
99+
key = keys[0]
100+
101+
if isinstance(key, str) and len(keys) > 1:
102+
if key in data:
103+
data[key].update(self._populate_data({}, keys[1:], value))
104+
else:
105+
data[key] = {}
106+
data[key].update(self._populate_data({}, keys[1:], value))
107+
elif isinstance(key, str):
108+
if key in data:
109+
data[key].update(self._populate_data({}, keys[1:], value))
110+
else:
111+
data[key] = value
112+
return data

click_types/coding.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Module for custom click types regarding to development"""
2+
3+
import semver
4+
5+
from click import ParamType
6+
7+
8+
class SemVerParamType(ParamType):
9+
"""Provide a custom click type for semantic versions.
10+
11+
This custom click type provides validity checks for semantic versions.
12+
"""
13+
name = 'semver'
14+
15+
def convert(self, value, param, ctx):
16+
"""Converts the value from string into semver type.
17+
18+
This method takes a string and check if this string belongs to semantic verstion definition.
19+
If the test is passed the value will be returned. If not a error message will be prompted.
20+
21+
:param value: the value passed
22+
:type value: str
23+
:param param: the parameter that we declared
24+
:type param: str
25+
:param ctx: context of the command
26+
:type ctx: str
27+
:return: the passed value as a checked semver
28+
:rtype: str
29+
"""
30+
try:
31+
semver.parse(value)
32+
return value
33+
except ValueError as e:
34+
self.fail('Not a valid version, {0}'.format(str(e)), param, ctx)

click_types/net.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Module for custom click types regarding to network"""
2+
3+
import click
4+
import ipaddress
5+
import re
6+
7+
8+
class CIDRParamType(click.ParamType):
9+
"""Provide a custom click type for network cidr handling.
10+
11+
This custom click type provides validity check for network cidrs.
12+
Both ip version (v4 and v6) are supported.
13+
"""
14+
name = "cidr"
15+
16+
def convert(self, value, param, ctx):
17+
"""Converts the value from string into cidr type.
18+
19+
This method takes a string and check if this string belongs to ipv4 or ipv6 cidr definition.
20+
If the test is passed the value will be returned. If not a error message will be prompted.
21+
22+
:param value: the value passed
23+
:type value: str
24+
:param param: the parameter that we declared
25+
:type param: str
26+
:param ctx: context of the command
27+
:type ctx: str
28+
:return: the passed value as a checked cidr
29+
:rtype: int
30+
"""
31+
try:
32+
if "/" in value:
33+
if "." in value and ":" not in value:
34+
ipaddress.IPv4Network(value)
35+
elif ":" in value and "." not in value:
36+
ipaddress.IPv6Network(value)
37+
else:
38+
raise ValueError('{0} has not bit mask set'.format(value))
39+
except (ValueError, ipaddress.AddressValueError) as e:
40+
self.fail('Not a network cidr, {0}'.format(str(e)), param, ctx)
41+
42+
return value
43+
44+
45+
class VlanIDParamType(click.ParamType):
46+
"""Provide a custom click type for vlan id handling.
47+
48+
This custom click type provides validity checks for vlan ids according to IEEE 802.1Q standard.
49+
"""
50+
name = 'vlanid'
51+
52+
def convert(self, value, param, ctx):
53+
"""Converts the value from string into semver type.
54+
55+
This method tages a string and check if this string belongs to semantic verstion definition.
56+
If the test is passed the value will be returned. If not a error message will be prompted.
57+
58+
:param value: the value passed
59+
:type value: str
60+
:param param: the parameter that we declared
61+
:type param: str
62+
:param ctx: context of the command
63+
:type ctx: str
64+
:return: the passed value as a checked vlan id
65+
:rtype: int
66+
"""
67+
try:
68+
if re.match(r'^[\d]{1,4}$', value):
69+
if 1 <= int(value) <= 4094:
70+
return int(value)
71+
else:
72+
raise ValueError('{0} is not within valid vlan id range'.format(value))
73+
else:
74+
raise ValueError('{0} is not match vlan id pattern'.format(value))
75+
except ValueError as e:
76+
self.fail('Not a valid vlan id, {0}'.format(str(e)), param, ctx)

requirements-dev.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ansible
2+
coverage
3+
ipaddress
4+
flake8
5+
pytest
6+
pytest-click
7+
semver
8+
wheel

setup.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from setuptools import setup, find_packages
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setup(
7+
name="click-types",
8+
version="1.0.0",
9+
author="Christian Meißner",
10+
author_email="Christian Meißner <cme@codeaffen.org>",
11+
description="Python library that provides useful click types",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
license="GPLv3",
15+
platform="Independent",
16+
url="https://codeaffen.org/projects/click-types/",
17+
packages=find_packages(exclude=['tests']),
18+
classifiers=[
19+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
20+
"Programming Language :: Python :: 3",
21+
"Intended Audience :: Developers",
22+
],
23+
keywords='click types',
24+
python_requires='>=3.6',
25+
install_requires=[
26+
'ansible',
27+
'ipaddress',
28+
'click',
29+
'semver'
30+
],
31+
)

0 commit comments

Comments
 (0)