Commits (2)
[MASTER]
init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))"
ignore=third_party
ignore-patterns=.venv
ignore-patterns=.venv,schema.py
disable=
missing-module-docstring,
missing-function-docstring,
......@@ -13,6 +13,7 @@ disable=
attribute-defined-outside-init,
logging-fstring-interpolation,
no-self-use
print-statement
......
......@@ -11,7 +11,7 @@ pydantic = "*"
sentry-sdk = "*"
click = "*"
sway = {editable = true, path = "."}
pyinquirer = {git = "https://github.com/CITGuru/PyInquirer.git"}
questionary = "*"
pyyaml = "*"
email-validator = "*"
......
{
"_meta": {
"hash": {
"sha256": "f60f1a761c0b0fc47ec9fc94d7f371908a39fdcae94989e9276a944d5ae14570"
"sha256": "103b36620501c0101d3d2c411b0c4231e7bcdb8d8c245e85f2ffd9855c3d7fe2"
},
"pipfile-spec": 6,
"requires": {
......@@ -105,18 +105,6 @@
"index": "pypi",
"version": "==1.9.1"
},
"pygments": {
"hashes": [
"sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb",
"sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"
],
"markers": "python_version >= '3.6'",
"version": "==2.12.0"
},
"pyinquirer": {
"git": "https://github.com/CITGuru/PyInquirer.git",
"ref": "7637373429bec66788650cda8091b7a6f12929ee"
},
"pyyaml": {
"hashes": [
"sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
......@@ -156,6 +144,14 @@
"index": "pypi",
"version": "==6.0"
},
"questionary": {
"hashes": [
"sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90",
"sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"
],
"index": "pypi",
"version": "==1.10.0"
},
"sentry-sdk": {
"hashes": [
"sha256:259535ba66933eacf85ab46524188c84dcb4c39f40348455ce15e2c0aca68863",
......@@ -290,7 +286,7 @@
"sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7",
"sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"
],
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
"markers": "python_full_version >= '3.6.1' and python_version < '4.0'",
"version": "==5.10.1"
},
"lazy-object-proxy": {
......@@ -346,32 +342,32 @@
},
"mypy": {
"hashes": [
"sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d",
"sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8",
"sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de",
"sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038",
"sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed",
"sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334",
"sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff",
"sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2",
"sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22",
"sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2",
"sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2",
"sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605",
"sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb",
"sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519",
"sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0",
"sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc",
"sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b",
"sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f",
"sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075",
"sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef",
"sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb",
"sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a",
"sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"
"sha256:0ebfb3f414204b98c06791af37a3a96772203da60636e2897408517fcfeee7a8",
"sha256:239d6b2242d6c7f5822163ee082ef7a28ee02e7ac86c35593ef923796826a385",
"sha256:29dc94d9215c3eb80ac3c2ad29d0c22628accfb060348fd23d73abe3ace6c10d",
"sha256:2c7f8bb9619290836a4e167e2ef1f2cf14d70e0bc36c04441e41487456561409",
"sha256:33d53a232bb79057f33332dbbb6393e68acbcb776d2f571ba4b1d50a2c8ba873",
"sha256:3a3e525cd76c2c4f90f1449fd034ba21fcca68050ff7c8397bb7dd25dd8b8248",
"sha256:3eabcbd2525f295da322dff8175258f3fc4c3eb53f6d1929644ef4d99b92e72d",
"sha256:481f98c6b24383188c928f33dd2f0776690807e12e9989dd0419edd5c74aa53b",
"sha256:7a76dc4f91e92db119b1be293892df8379b08fd31795bb44e0ff84256d34c251",
"sha256:7d390248ec07fa344b9f365e6ed9d205bd0205e485c555bed37c4235c868e9d5",
"sha256:826a2917c275e2ee05b7c7b736c1e6549a35b7ea5a198ca457f8c2ebea2cbecf",
"sha256:85cf2b14d32b61db24ade8ac9ae7691bdfc572a403e3cb8537da936e74713275",
"sha256:8d645e9e7f7a5da3ec3bbcc314ebb9bb22c7ce39e70367830eb3c08d0140b9ce",
"sha256:925aa84369a07846b7f3b8556ccade1f371aa554f2bd4fb31cb97a24b73b036e",
"sha256:a85a20b43fa69efc0b955eba1db435e2ffecb1ca695fe359768e0503b91ea89f",
"sha256:bfd4f6536bd384c27c392a8b8f790fd0ed5c0cf2f63fc2fed7bce56751d53026",
"sha256:cb7752b24528c118a7403ee955b6a578bfcf5879d5ee91790667c8ea511d2085",
"sha256:cc537885891382e08129d9862553b3d00d4be3eb15b8cae9e2466452f52b0117",
"sha256:d4fccf04c1acf750babd74252e0f2db6bd2ac3aa8fe960797d9f3ef41cf2bfd4",
"sha256:f1ba54d440d4feee49d8768ea952137316d454b15301c44403db3f2cb51af024",
"sha256:f47322796c412271f5aea48381a528a613f33e0a115452d03ae35d673e6064f8",
"sha256:fbfb873cf2b8d8c3c513367febde932e061a5f73f762896826ba06391d932b2a",
"sha256:ffdad80a92c100d1b0fe3d3cf1a4724136029a29afe8566404c0146747114382"
],
"index": "pypi",
"version": "==0.950"
"version": "==0.960"
},
"mypy-extensions": {
"hashes": [
......@@ -527,7 +523,7 @@
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version >= '3.7'",
"markers": "python_version < '3.11'",
"version": "==2.0.1"
},
"typing-extensions": {
......
This diff is collapsed.
- message: |
During this wizard you will be prompted for:
✅ Services you wish to install
✅ The personal domain you purchased from namecheap
✅ Namecheap username and API key
✅ An email address to notify (these can all be turned off) when:
- auto renewal fails on you encryption certs
- system backup fails
Feel free to cancel and gather the required info before installing.
Continue?
name: is_ok
type: confirm
- type: input
name: personal_email
message: "What's the email used to manage personal services (protonmail recommended)?"
- type: input
name: personal_domain
message: "What's the personal domain you got from namecheap?"
type: checkbox
message: "Cryptocurrencies to mine:"
name: cryptos
qmark:
choices:
- name: bitcoin
disabled: required
checked: true
- name: openethereum
- name: litecoind
message: |
During this wizard you will be prompted for:
✅ Services you wish to install
✅ The personal domain you purchased from namecheap
✅ Namecheap username and API key
✅ An email address to notify (these can all be turned off) when:
- auto renewal fails on your encryption certs
- system backup fails
Feel free to cancel and gather the required info before installing.
Continue?
name: continue
type: confirm
type: checkbox
message: "Optional Services:"
qmark: "🤔"
name: options
choices:
- name: "Collabora: Do you want a web-based libre office?"
- name: "CPUMiner: Do you want to mine cryptocurrencies?"
type: checkbox
message: "Personal Services:"
qmark: "🔒"
name: services
choices:
- separator: "📺 Media"
- name: "Funkwhale: Federated Grooveshark inspired music server\n Dependencies: redis postgres"
- name: "Jellyfin: Media Server (6.5k⭐)"
- name: "Audiobookshelf: Alternative to audible (812⭐)\n Dependencies: syncthing"
- name: "Calibre-Web: Read, upload and share books (3.2k⭐)\n Dependencies: syncthing"
- separator: "👪 Groupware"
- name: "Nextcloud: Alternative to Google Docs/Calendar/Contacts (19.3k⭐)\n Dependencies: mariadb redis. Optional: collabora"
- name: "Paperless: Document management with OCR (5.6k⭐)\n Dependencies: redis postgres"
- name: "LibrePhotos: Alternative to google photos (5.6k⭐)"
- name: "Hydroxide: Use protonmail with your own mail clients (545⭐)"
- name: "Joplin: Markdown notes with vim and emacs mode (20k⭐)"
- separator: "💬 Social Media"
- name: "Weechat: Manage slack, gitter, discord, irc, matrix chats (1.9k⭐)"
- name: "Mastodon: Federated Twitter (29.3k⭐)"
- name: "Fluent-Reader: An RSS Reader (2.8k⭐)\n Dependencies: syncthing"
- separator: "🥗 Miscellaneous"
- name: "Bitwarden: Manage passwords, secrets and tokens (3.7k⭐)"
disabled: "required"
checked: true
- name: "Bitcore: Manage crypto. Compatible with bitpay debit card (3.6k⭐)\n Dependencies: mongodb bitcoind. Optional: cpuminer ethereum"
- name: "Mycroft: Personal assistant. Easily add your own module (6.8k⭐)\n Dependencies: redis postgres"
- name: "wger: Alternative to google/apple fit (414⭐).\n Dependencies: redis postgres"
\ No newline at end of file
- name: personal_email
type: text
message: "What's the email used to manage personal services (protonmail recommended)?"
qmark: ' '
- name: personal_domain
type: text
message: "What's the personal domain you got from namecheap?"
qmark: 🌎
- name: namecheap_username
type: text
message: "What's your namecheap username?"
qmark:
- name: namecheap_api_key
type: password
qmark: 🔑
message: "What's your namecheap API key?"
- name: master_password
type: password
qmark: 🔑
message: 'What is the master password you will use to access bitwarden password manager?'
{
"$id": "https://minio.sway.cx/shared/data/schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "Resource",
"required": [
"name",
"description",
"category",
"repo-data"
],
"properties": {
"options": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/$defs/web-client"
},
{
"$ref": "#/$defs/personal-service"
},
{
"$ref": "#/$defs/daemon"
}
]
}
},
"dependencies": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/$defs/personal-service"
},
{
"$ref": "#/$defs/database"
},
{
"$ref": "#/$defs/daemon"
}
]
}
},
"name": {
"oneOf": [
{
"$ref": "#/$defs/personal-service"
},
{
"$ref": "#/$defs/dev-service"
},
{
"$ref": "#/$defs/android-client"
},
{
"$ref": "#/$defs/terminal-client"
},
{
"$ref": "#/$defs/web-client"
},
{
"$ref": "#/$defs/operating-system"
},
{
"$ref": "#/$defs/daemon"
},
{
"$ref": "#/$defs/database"
}
]
},
"description": {
"type": "string"
},
"category": {
"type": "string",
"enum": [
"personal-services",
"dev-services",
"android-clients",
"web-clients",
"operating-systems",
"terminal-clients",
"daemons",
"databases"
]
},
"repo-data": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "object",
"required": [
"url",
"title"
],
"properties": {
"url": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"rating": {
"type": "integer"
}
}
}
}
},
"$defs": {
"personal-service": {
"type": "string",
"enum": [
"audiobookshelf",
"bitcore",
"calibre-web",
"collabora",
"fluent-reader",
"funkwhale",
"home-assistant",
"hydroxide",
"jellyfin",
"joplin",
"mastodon",
"mycroft",
"nextcloud",
"paperless",
"librephotos",
"syncthing",
"weechat",
"wger"
]
},
"dev-service": {
"type": "string",
"enum": [
"bitwarden",
"dokku",
"emqx",
"garie",
"gitlab",
"gorush",
"hasura",
"kafka",
"keycloak",
"kong",
"loki",
"matrix",
"minio",
"postal",
"sentry",
"textbelt",
"traefik"
]
},
"web-client": {
"type": "string",
"enum": [
"glowing-bear"
]
},
"terminal-client": {
"type": "string",
"enum": [
"castero",
"cava",
"emacs",
"epr",
"feh",
"khal",
"khard",
"mpv",
"ncmpcpp",
"neomutt",
"newsboat",
"ranger"
]
},
"android-client": {
"type": "string",
"enum": [
"antennapod",
"bitpay",
"geometric-weather",
"k9-mail",
"librera",
"microg",
"orgzly",
"simple-calendar",
"simple-contacts",
"simple-gallery",
"termux",
"tusky",
"weechat-android"
]
},
"operating-system": {
"type": "string",
"enum": [
"arch-linux",
"asteroidos",
"debian",
"lineageos"
]
},
"daemon": {
"type": "string",
"enum": [
"docker",
"restic",
"bitcoind",
"openethereum",
"cpuminer",
"litecoind"
]
},
"database": {
"type": "string",
"enum": [
"mariadb",
"mongodb",
"postgres",
"redis"
]
}
}
}
......@@ -62,3 +62,8 @@ def plugin(plugin_file: str) -> None:
@cli.command()
def update() -> None:
click.echo("updates")
@cli.command()
def autocomplete() -> None:
click.echo("autocomplete")
from typing import List, Optional, Union, Literal, Tuple
from pydantic import BaseModel, EmailStr, Field
from schema import PersonalService, Daemon, Database, DevService
DOMAIN_REGEX = r"^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$"
KEY_REGEX = r"^[0-9a-fA-F]{32}"
PASSWORD_REGEX = r"^.*(?=.{14,})(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*]).*$" # nosec B105
ServicePromptResponse = Tuple[List[PersonalService], List[Database], List[Daemon]]
class Question(BaseModel):
type: Literal["confirm", "text", "password", "checkbox"]
name: str
message: str
when: Optional[bool]
class Name(BaseModel):
name: str
checked: Optional[bool]
disabled: Optional[str]
class Separator(BaseModel):
separator: str
Choices = List[Union[Name, Separator]]
class CheckboxQuestion(Question):
qmark: str
choices: Choices
class Variables(BaseModel):
personal_email: EmailStr
personal_domain: str = Field(
..., regex=r"^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$"
)
personal_domain: str = Field(..., regex=DOMAIN_REGEX)
namecheap_username: str
namecheap_api_key: str
master_password: str
class Spec(BaseModel):
variables: Variables
services: List[Union[PersonalService, DevService]]
databases: List[Database]
daemons: List[Daemon]
# generated by datamodel-codegen:
# filename: schema.json
# timestamp: 2022-05-28T18:45:15+00:00
from __future__ import annotations
from enum import Enum
from typing import List, Optional, Union
from pydantic import AnyUrl, BaseModel, Field
class Category(Enum):
personal_services = "personal-services"
dev_services = "dev-services"
android_clients = "android-clients"
web_clients = "web-clients"
operating_systems = "operating-systems"
terminal_clients = "terminal-clients"
daemons = "daemons"
databases = "databases"
class RepoDatum(BaseModel):
url: AnyUrl
title: str
rating: Optional[int] = None
class AndroidClient(Enum):
antennapod = "antennapod"
bitpay = "bitpay"
geometric_weather = "geometric-weather"
k9_mail = "k9-mail"
librera = "librera"
microg = "microg"
orgzly = "orgzly"
simple_calendar = "simple-calendar"
simple_contacts = "simple-contacts"
simple_gallery = "simple-gallery"
termux = "termux"
tusky = "tusky"
weechat_android = "weechat-android"
class Daemon(Enum):
docker = "docker"
restic = "restic"
bitcoind = "bitcoind"
openethereum = "openethereum"
cpuminer = "cpuminer"
litecoind = "litecoind"
class Database(Enum):
mariadb = "mariadb"
mongodb = "mongodb"
postgres = "postgres"
redis = "redis"
class DevService(Enum):
bitwarden = "bitwarden"
dokku = "dokku"
emqx = "emqx"
garie = "garie"
gitlab = "gitlab"
gorush = "gorush"
hasura = "hasura"
kafka = "kafka"
keycloak = "keycloak"
kong = "kong"
loki = "loki"
matrix = "matrix"
minio = "minio"
postal = "postal"
sentry = "sentry"
textbelt = "textbelt"
traefik = "traefik"
class OperatingSystem(Enum):
arch_linux = "arch-linux"
asteroidos = "asteroidos"
debian = "debian"
lineageos = "lineageos"
class PersonalService(Enum):
audiobookshelf = "audiobookshelf"
bitcore = "bitcore"
calibre_web = "calibre-web"
collabora = "collabora"
fluent_reader = "fluent-reader"
funkwhale = "funkwhale"
home_assistant = "home-assistant"
hydroxide = "hydroxide"
jellyfin = "jellyfin"
joplin = "joplin"
mastodon = "mastodon"
mycroft = "mycroft"
nextcloud = "nextcloud"
paperless = "paperless"
librephotos = "librephotos"
syncthing = "syncthing"
weechat = "weechat"
wger = "wger"
class TerminalClient(Enum):
castero = "castero"
cava = "cava"
emacs = "emacs"
epr = "epr"
feh = "feh"
khal = "khal"
khard = "khard"
mpv = "mpv"
ncmpcpp = "ncmpcpp"
neomutt = "neomutt"
newsboat = "newsboat"
ranger = "ranger"
class WebClient(Enum):
glowing_bear = "glowing-bear"
class Resource(BaseModel):
options: Optional[List[Union[WebClient, PersonalService, Daemon]]] = None
dependencies: Optional[List[Union[PersonalService, Database, Daemon]]] = None
name: Union[
PersonalService,
DevService,
AndroidClient,
TerminalClient,
WebClient,
OperatingSystem,
Daemon,
Database,
]
description: str
category: Category
repo_data: List[RepoDatum] = Field(
..., alias="repo-data", min_items=1, unique_items=True
)
import sys
from typing import List, cast, Any
import questionary
from PyInquirer.prompt import prompt
from components import intro, error_message, no_prob
from models import Question, Variables, CheckboxQuestion, Spec
from schema import PersonalService, Daemon, Resource, Database
from components import intro, no_prob
from utils import get_prompt, retry
from utils import (
get_questions,
convert_separators,
get_deps_and_opts,
filter_options,
answers2types,
inputs_prompt,
)
def process_initial_question(proceed: bool = True, **kwargs: Any) -> bool:
if not proceed:
return False
initial_question = get_questions("personal/initial.yml", Question)
initial_answer = questionary.prompt(initial_question.dict(), **kwargs)
if "continue" not in initial_answer:
no_prob()
sys.exit()
return bool(initial_answer["continue"])
def process_variables(proceed: bool, **kwargs: Any) -> Variables:
if not proceed:
sys.exit()
variable_questions = get_questions("personal/variables.yml", Question)
variables_answers = inputs_prompt(
[q.dict() for q in variable_questions], Variables, **kwargs
)
if not variables_answers:
sys.exit()
return variables_answers
def process_services(
proceed: bool,
**kwargs: Any,
) -> List[str]:
if not proceed:
sys.exit()
service_questions = get_questions("personal/services.yml", CheckboxQuestion)
updated_choices = convert_separators(service_questions.choices)
service_questions.choices = updated_choices
style = questionary.Style([("selected", "noreverse")])
answers = questionary.prompt(service_questions.dict(), style=style, **kwargs)
if not answers:
sys.exit()
if answers == {"services": []}:
error_message("You must select at least one service")
sys.exit()
return cast(List[str], answers["services"])
def process_options(proceed: bool, opts: List[str], **kwargs: Any) -> List[str]:
if not proceed:
sys.exit()
option_questions = get_questions("personal/options.yml", CheckboxQuestion)
filtered_opt_questions = filter_options(opts, option_questions)
if not filtered_opt_questions.choices:
# no options to select
return []
answers = questionary.prompt(filtered_opt_questions.dict(), **kwargs)
return cast(List[str], answers["options"])
def process_cryptos(proceed: bool, opt_names: List[str], **kwargs: Any) -> List[str]:
if not proceed:
return []
for opt in opt_names:
if "CPUMiner" in opt:
cryptos_question = get_questions("personal/cryptos.yml", CheckboxQuestion)
answers = questionary.prompt(cryptos_question.dict(), **kwargs)
if "cryptos" not in answers:
# no options to select
return []
return cast(List[str], answers["cryptos"])
return []
def create_spec(variables: Variables, resources: List[Resource]) -> Spec:
personal_services = [r for r in resources if isinstance(r, PersonalService)]
daemons = [r for r in resources if isinstance(r, Daemon)]
databases = [r for r in resources if isinstance(r, Database)]
return Spec(
variables=variables,
services=personal_services,
daemons=daemons,
databases=databases,
)
def install() -> None:
intro("Personal Services 🔒")
initial_prompt = get_prompt("personal/initial.yml")
initial_answer = prompt(initial_prompt)
if not initial_answer:
sys.exit()
if not initial_answer["is_ok"]:
no_prob()
if initial_answer["is_ok"]:
variables_prompt = get_prompt("personal/variables.yml")
for var in variables_prompt:
retry(var)
proceed = process_initial_question()
variables = process_variables(proceed)
service_answers = process_services(any(variables.dict().values()))
services = answers2types(service_answers)
dependency_answers, option_answers = get_deps_and_opts(service_answers)
dependencies = answers2types(dependency_answers)
selection_answers = process_options(bool(option_answers), option_answers)
selections = answers2types(selection_answers)
cryptos_answers = process_cryptos(bool(selection_answers), selection_answers)
cryptos = answers2types(cryptos_answers)
spec = create_spec(variables, services + dependencies + selections + cryptos)
print(spec)