From c43f3e10ddfb9497886b5943ae54a43e9534edc3 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 14:17:04 +0800 Subject: [PATCH 1/8] feat: add spec --- compose_viz/cli.py | 2 +- compose_viz/graph.py | 2 +- compose_viz/models/__init__.py | 0 compose_viz/{ => models}/compose.py | 2 +- compose_viz/{ => models}/extends.py | 0 compose_viz/{ => models}/port.py | 0 compose_viz/{ => models}/service.py | 6 +- compose_viz/{ => models}/viz_formats.py | 0 compose_viz/{ => models}/volume.py | 0 compose_viz/parser.py | 8 +- compose_viz/spec/__init__.py | 0 compose_viz/spec/compose-spec.json | 813 ++++++++++++++++++++++++ compose_viz/spec/compose_spec.py | 550 ++++++++++++++++ pyproject.toml | 4 + tests/test_extends.py | 2 +- tests/test_parse_file.py | 10 +- tests/test_port.py | 2 +- tests/test_volume.py | 2 +- 18 files changed, 1385 insertions(+), 18 deletions(-) create mode 100644 compose_viz/models/__init__.py rename compose_viz/{ => models}/compose.py (81%) rename compose_viz/{ => models}/extends.py (100%) rename compose_viz/{ => models}/port.py (100%) rename compose_viz/{ => models}/service.py (89%) rename compose_viz/{ => models}/viz_formats.py (100%) rename compose_viz/{ => models}/volume.py (100%) create mode 100644 compose_viz/spec/__init__.py create mode 100644 compose_viz/spec/compose-spec.json create mode 100644 compose_viz/spec/compose_spec.py diff --git a/compose_viz/cli.py b/compose_viz/cli.py index ffb10bd..618c6cc 100644 --- a/compose_viz/cli.py +++ b/compose_viz/cli.py @@ -4,8 +4,8 @@ import typer from compose_viz import __app_name__, __version__ from compose_viz.graph import Graph +from compose_viz.models.viz_formats import VizFormats from compose_viz.parser import Parser -from compose_viz.viz_formats import VizFormats app = typer.Typer( invoke_without_command=True, diff --git a/compose_viz/graph.py b/compose_viz/graph.py index 32c5569..3f634f2 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -2,7 +2,7 @@ from typing import Optional import graphviz -from compose_viz.compose import Compose +from compose_viz.models.compose import Compose def apply_vertex_style(type) -> dict: diff --git a/compose_viz/models/__init__.py b/compose_viz/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose_viz/compose.py b/compose_viz/models/compose.py similarity index 81% rename from compose_viz/compose.py rename to compose_viz/models/compose.py index 03e8f74..441b5e6 100644 --- a/compose_viz/compose.py +++ b/compose_viz/models/compose.py @@ -1,6 +1,6 @@ from typing import List -from compose_viz.service import Service +from compose_viz.models.service import Service class Compose: diff --git a/compose_viz/extends.py b/compose_viz/models/extends.py similarity index 100% rename from compose_viz/extends.py rename to compose_viz/models/extends.py diff --git a/compose_viz/port.py b/compose_viz/models/port.py similarity index 100% rename from compose_viz/port.py rename to compose_viz/models/port.py diff --git a/compose_viz/service.py b/compose_viz/models/service.py similarity index 89% rename from compose_viz/service.py rename to compose_viz/models/service.py index b1bf84b..89740ba 100644 --- a/compose_viz/service.py +++ b/compose_viz/models/service.py @@ -1,8 +1,8 @@ from typing import List, Optional -from compose_viz.extends import Extends -from compose_viz.port import Port -from compose_viz.volume import Volume +from compose_viz.models.extends import Extends +from compose_viz.models.port import Port +from compose_viz.models.volume import Volume class Service: diff --git a/compose_viz/viz_formats.py b/compose_viz/models/viz_formats.py similarity index 100% rename from compose_viz/viz_formats.py rename to compose_viz/models/viz_formats.py diff --git a/compose_viz/volume.py b/compose_viz/models/volume.py similarity index 100% rename from compose_viz/volume.py rename to compose_viz/models/volume.py diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 757d3ed..96e2667 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -3,10 +3,10 @@ from typing import Dict, List, Optional from ruamel.yaml import YAML -from compose_viz.compose import Compose, Service -from compose_viz.extends import Extends -from compose_viz.port import Port, Protocol -from compose_viz.volume import Volume, VolumeType +from compose_viz.models.compose import Compose, Service +from compose_viz.models.extends import Extends +from compose_viz.models.port import Port, Protocol +from compose_viz.models.volume import Volume, VolumeType class Parser: diff --git a/compose_viz/spec/__init__.py b/compose_viz/spec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compose_viz/spec/compose-spec.json b/compose_viz/spec/compose-spec.json new file mode 100644 index 0000000..d83dc2d --- /dev/null +++ b/compose_viz/spec/compose-spec.json @@ -0,0 +1,813 @@ +{ + "$schema": "http://json-schema.org/draft/2019-09/schema#", + "id": "compose_spec.json", + "type": "object", + "title": "Compose Specification", + "description": "The Compose file is a YAML file defining a multi-containers based application.", + + "properties": { + "version": { + "type": "string", + "description": "declared for backward compatibility, ignored." + }, + + "name": { + "type": "string", + "description": "define the Compose project name, until user defines one explicitly." + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + }, + + "secrets": { + "id": "#/properties/secrets", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/secret" + } + }, + "additionalProperties": false + }, + + "configs": { + "id": "#/properties/configs", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/config" + } + }, + "additionalProperties": false + } + }, + + "patternProperties": {"^x-": {}}, + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "deploy": {"$ref": "#/definitions/deployment"}, + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"}, + "ssh": {"$ref": "#/definitions/list_or_dict"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "cache_from": {"type": "array", "items": {"type": "string"}}, + "cache_to": {"type": "array", "items": {"type": "string"}}, + "no_cache": {"type": "boolean"}, + "network": {"type": "string"}, + "pull": {"type": "boolean"}, + "target": {"type": "string"}, + "shm_size": {"type": ["integer", "string"]}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "isolation": {"type": "string"}, + "secrets": {"$ref": "#/definitions/service_config_or_secret"}, + "tags":{"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + }, + "blkio_config": { + "type": "object", + "properties": { + "device_read_bps": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "device_read_iops": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "device_write_bps": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "device_write_iops": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_limit"} + }, + "weight": {"type": "integer"}, + "weight_device": { + "type": "array", + "items": {"$ref": "#/definitions/blkio_weight"} + } + }, + "additionalProperties": false + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "configs": {"$ref": "#/definitions/service_config_or_secret"}, + "container_name": {"type": "string"}, + "cpu_count": {"type": "integer", "minimum": 0}, + "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, + "cpu_shares": {"type": ["number", "string"]}, + "cpu_quota": {"type": ["number", "string"]}, + "cpu_period": {"type": ["number", "string"]}, + "cpu_rt_period": {"type": ["number", "string"]}, + "cpu_rt_runtime": {"type": ["number", "string"]}, + "cpus": {"type": ["number", "string"]}, + "cpuset": {"type": "string"}, + "credential_spec": { + "type": "object", + "properties": { + "config": {"type": "string"}, + "file": {"type": "string"}, + "registry": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "depends_on": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "type": "object", + "additionalProperties": false, + "properties": { + "condition": { + "type": "string", + "enum": ["service_started", "service_healthy", "service_completed_successfully"] + } + }, + "required": ["condition"] + } + } + } + ] + }, + "device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + "extends": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + + "properties": { + "service": {"type": "string"}, + "file": {"type": "string"} + }, + "required": ["service"], + "additionalProperties": false + } + ] + }, + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "group_add": { + "type": "array", + "items": { + "type": ["string", "number"] + }, + "uniqueItems": true + }, + "healthcheck": {"$ref": "#/definitions/healthcheck"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "init": {"type": "boolean"}, + "ipc": {"type": "string"}, + "isolation": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "mac_address": {"type": "string"}, + "mem_limit": {"type": ["number", "string"]}, + "mem_reservation": {"type": ["string", "integer"]}, + "mem_swappiness": {"type": "integer"}, + "memswap_limit": {"type": ["number", "string"]}, + "network_mode": {"type": "string"}, + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"}, + "link_local_ips": {"$ref": "#/definitions/list_of_strings"}, + "priority": {"type": "number"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "oom_kill_disable": {"type": "boolean"}, + "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, + "pid": {"type": ["string", "null"]}, + "pids_limit": {"type": ["number", "string"]}, + "platform": {"type": "string"}, + "ports": { + "type": "array", + "items": { + "oneOf": [ + {"type": "number", "format": "ports"}, + {"type": "string", "format": "ports"}, + { + "type": "object", + "properties": { + "mode": {"type": "string"}, + "host_ip": {"type": "string"}, + "target": {"type": "integer"}, + "published": {"type": ["string", "integer"]}, + "protocol": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + }, + "uniqueItems": true + }, + "privileged": {"type": "boolean"}, + "profiles": {"$ref": "#/definitions/list_of_strings"}, + "pull_policy": {"type": "string", "enum": [ + "always", "never", "if_not_present", "build", "missing" + ]}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "runtime": { + "type": "string" + }, + "scale": { + "type": "integer" + }, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "secrets": {"$ref": "#/definitions/service_config_or_secret"}, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, + "stdin_open": {"type": "boolean"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, + "storage_opt": {"type": "object"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type": "object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + } + } + }, + "user": {"type": "string"}, + "userns_mode": {"type": "string"}, + "volumes": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "source": {"type": "string"}, + "target": {"type": "string"}, + "read_only": {"type": "boolean"}, + "consistency": {"type": "string"}, + "bind": { + "type": "object", + "properties": { + "propagation": {"type": "string"}, + "create_host_path": {"type": "boolean"}, + "selinux": {"type": "string", "enum": ["z", "Z"]} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "volume": { + "type": "object", + "properties": { + "nocopy": {"type": "boolean"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "tmpfs": { + "type": "object", + "properties": { + "size": { + "oneOf": [ + {"type": "integer", "minimum": 0}, + {"type": "string"} + ] + }, + "mode": {"type": "number"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + }, + "uniqueItems": true + }, + "volumes_from": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + "working_dir": {"type": "string"} + }, + "patternProperties": {"^x-": {}}, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": "object", + "properties": { + "disable": {"type": "boolean"}, + "interval": {"type": "string", "format": "duration"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "timeout": {"type": "string", "format": "duration"}, + "start_period": {"type": "string", "format": "duration"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "endpoint_mode": {"type": "string"}, + "replicas": {"type": "integer"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "rollback_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"}, + "order": {"type": "string", "enum": [ + "start-first", "stop-first" + ]} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "update_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"}, + "order": {"type": "string", "enum": [ + "start-first", "stop-first" + ]} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "resources": { + "type": "object", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpus": {"type": ["number", "string"]}, + "memory": {"type": "string"}, + "pids": {"type": "integer"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "reservations": { + "type": "object", + "properties": { + "cpus": {"type": ["number", "string"]}, + "memory": {"type": "string"}, + "generic_resources": {"$ref": "#/definitions/generic_resources"}, + "devices": {"$ref": "#/definitions/devices"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "restart_policy": { + "type": "object", + "properties": { + "condition": {"type": "string"}, + "delay": {"type": "string", "format": "duration"}, + "max_attempts": {"type": "integer"}, + "window": {"type": "string", "format": "duration"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "placement": { + "type": "object", + "properties": { + "constraints": {"type": "array", "items": {"type": "string"}}, + "preferences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "spread": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + "max_replicas_per_node": {"type": "integer"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + + "generic_resources": { + "id": "#/definitions/generic_resources", + "type": "array", + "items": { + "type": "object", + "properties": { + "discrete_resource_spec": { + "type": "object", + "properties": { + "kind": {"type": "string"}, + "value": {"type": "number"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + + "devices": { + "id": "#/definitions/devices", + "type": "array", + "items": { + "type": "object", + "properties": { + "capabilities": {"$ref": "#/definitions/list_of_strings"}, + "count": {"type": ["string", "integer"]}, + "device_ids": {"$ref": "#/definitions/list_of_strings"}, + "driver":{"type": "string"}, + "options":{"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + + "network": { + "id": "#/definitions/network", + "type": ["object", "null"], + "properties": { + "name": {"type": "string"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnet": {"type": "string", "format": "subnet_ip_address"}, + "ip_range": {"type": "string"}, + "gateway": {"type": "string"}, + "aux_addresses": { + "type": "object", + "additionalProperties": false, + "patternProperties": {"^.+$": {"type": "string"}} + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + }, + "options": { + "type": "object", + "additionalProperties": false, + "patternProperties": {"^.+$": {"type": "string"}} + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": { + "deprecated": true, + "type": "string" + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "internal": {"type": "boolean"}, + "enable_ipv6": {"type": "boolean"}, + "attachable": {"type": "boolean"}, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "name": {"type": "string"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": { + "deprecated": true, + "type": "string" + } + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + + "secret": { + "id": "#/definitions/secret", + "type": "object", + "properties": { + "name": {"type": "string"}, + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "template_driver": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + + "config": { + "id": "#/definitions/config", + "type": "object", + "properties": { + "name": {"type": "string"}, + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": { + "deprecated": true, + "type": "string" + } + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "template_driver": {"type": "string"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "boolean", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "blkio_limit": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "rate": {"type": ["integer", "string"]} + }, + "additionalProperties": false + }, + "blkio_weight": { + "type": "object", + "properties": { + "path": {"type": "string"}, + "weight": {"type": "integer"} + }, + "additionalProperties": false + }, + + "service_config_or_secret": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + }, + "additionalProperties": false, + "patternProperties": {"^x-": {}} + } + ] + } + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} diff --git a/compose_viz/spec/compose_spec.py b/compose_viz/spec/compose_spec.py new file mode 100644 index 0000000..0880ac6 --- /dev/null +++ b/compose_viz/spec/compose_spec.py @@ -0,0 +1,550 @@ +# generated by datamodel-codegen: +# filename: compose-spec.json +# timestamp: 2022-05-27T05:44:40+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Any, Dict, List, Optional, Union + +from pydantic import BaseModel, Extra, Field, conint, constr + + +class CredentialSpec(BaseModel): + class Config: + extra = Extra.forbid + + config: Optional[str] = None + file: Optional[str] = None + registry: Optional[str] = None + + +class Condition(Enum): + service_started = "service_started" + service_healthy = "service_healthy" + service_completed_successfully = "service_completed_successfully" + + +class DependsOn(BaseModel): + class Config: + extra = Extra.forbid + + condition: Condition + + +class Extend(BaseModel): + class Config: + extra = Extra.forbid + + service: str + file: Optional[str] = None + + +class Logging(BaseModel): + class Config: + extra = Extra.forbid + + driver: Optional[str] = None + options: Optional[Dict[constr(regex=r"^.+$"), Optional[Union[str, float]]]] = None # type: ignore # noqa: F722 + + +class Port(BaseModel): + class Config: + extra = Extra.forbid + + mode: Optional[str] = None + host_ip: Optional[str] = None + target: Optional[int] = None + published: Optional[Union[str, int]] = None + protocol: Optional[str] = None + + +class PullPolicy(Enum): + always = "always" + never = "never" + if_not_present = "if_not_present" + build = "build" + missing = "missing" + + +class Ulimit(BaseModel): + class Config: + extra = Extra.forbid + + hard: int + soft: int + + +class Selinux(Enum): + z = "z" + Z = "Z" + + +class Bind(BaseModel): + class Config: + extra = Extra.forbid + + propagation: Optional[str] = None + create_host_path: Optional[bool] = None + selinux: Optional[Selinux] = None + + +class AdditionalVolumeOption(BaseModel): + class Config: + extra = Extra.forbid + + nocopy: Optional[bool] = None + + +class Tmpfs(BaseModel): + class Config: + extra = Extra.forbid + + size: Optional[Union[conint(ge=0), str]] = None # type: ignore + mode: Optional[float] = None + + +class ServiceVolume(BaseModel): + class Config: + extra = Extra.forbid + + type: str + source: Optional[str] = None + target: Optional[str] = None + read_only: Optional[bool] = None + consistency: Optional[str] = None + bind: Optional[Bind] = None + volume: Optional[AdditionalVolumeOption] = None + tmpfs: Optional[Tmpfs] = None + + +class Healthcheck(BaseModel): + class Config: + extra = Extra.forbid + + disable: Optional[bool] = None + interval: Optional[str] = None + retries: Optional[float] = None + test: Optional[Union[str, List[str]]] = None + timeout: Optional[str] = None + start_period: Optional[str] = None + + +class Order(Enum): + start_first = "start-first" + stop_first = "stop-first" + + +class RollbackConfig(BaseModel): + class Config: + extra = Extra.forbid + + parallelism: Optional[int] = None + delay: Optional[str] = None + failure_action: Optional[str] = None + monitor: Optional[str] = None + max_failure_ratio: Optional[float] = None + order: Optional[Order] = None + + +class ConfigOrder(Enum): + start_first = "start-first" + stop_first = "stop-first" + + +class UpdateConfig(BaseModel): + class Config: + extra = Extra.forbid + + parallelism: Optional[int] = None + delay: Optional[str] = None + failure_action: Optional[str] = None + monitor: Optional[str] = None + max_failure_ratio: Optional[float] = None + order: Optional[ConfigOrder] = None + + +class Limits(BaseModel): + class Config: + extra = Extra.forbid + + cpus: Optional[Union[float, str]] = None + memory: Optional[str] = None + pids: Optional[int] = None + + +class RestartPolicy(BaseModel): + class Config: + extra = Extra.forbid + + condition: Optional[str] = None + delay: Optional[str] = None + max_attempts: Optional[int] = None + window: Optional[str] = None + + +class Preference(BaseModel): + class Config: + extra = Extra.forbid + + spread: Optional[str] = None + + +class Placement(BaseModel): + class Config: + extra = Extra.forbid + + constraints: Optional[List[str]] = None + preferences: Optional[List[Preference]] = None + max_replicas_per_node: Optional[int] = None + + +class DiscreteResourceSpec(BaseModel): + class Config: + extra = Extra.forbid + + kind: Optional[str] = None + value: Optional[float] = None + + +class GenericResource(BaseModel): + class Config: + extra = Extra.forbid + + discrete_resource_spec: Optional[DiscreteResourceSpec] = None + + +class GenericResources(BaseModel): + __root__: List[GenericResource] + + +class ConfigItem(BaseModel): + class Config: + extra = Extra.forbid + + subnet: Optional[str] = None + ip_range: Optional[str] = None + gateway: Optional[str] = None + aux_addresses: Optional[Dict[constr(regex=r"^.+$"), str]] = None # type: ignore # noqa: F722 + + +class Ipam(BaseModel): + class Config: + extra = Extra.forbid + + driver: Optional[str] = None + config: Optional[List[ConfigItem]] = None + options: Optional[Dict[constr(regex=r"^.+$"), str]] = None # type: ignore # noqa: F722 + + +class ExternalNetwork(BaseModel): + class Config: + extra = Extra.forbid + + name: Optional[str] = None + + +class ExternalVolume(BaseModel): + class Config: + extra = Extra.forbid + + name: Optional[str] = None + + +class ExternalSecret(BaseModel): + name: Optional[str] = None + + +class ExternalConfig(BaseModel): + name: Optional[str] = None + + +class ListOfStrings(BaseModel): + __root__: List[str] = Field(..., unique_items=True) + + +class ListOrDict(BaseModel): + __root__: Union[Dict[constr(regex=r".+"), Optional[Union[str, float, bool]]], List[str]] # type: ignore # noqa: F722, E501 + + +class BlkioLimit(BaseModel): + class Config: + extra = Extra.forbid + + path: Optional[str] = None + rate: Optional[Union[int, str]] = None + + +class BlkioWeight(BaseModel): + class Config: + extra = Extra.forbid + + path: Optional[str] = None + weight: Optional[int] = None + + +class ServiceConfigOrSecretItem(BaseModel): + class Config: + extra = Extra.forbid + + source: Optional[str] = None + target: Optional[str] = None + uid: Optional[str] = None + gid: Optional[str] = None + mode: Optional[float] = None + + +class ServiceConfigOrSecret(BaseModel): + __root__: List[Union[str, ServiceConfigOrSecretItem]] + + +class Constraints(BaseModel): + __root__: Any + + +class BuildItem(BaseModel): + class Config: + extra = Extra.forbid + + context: Optional[str] = None + dockerfile: Optional[str] = None + args: Optional[ListOrDict] = None + ssh: Optional[ListOrDict] = None + labels: Optional[ListOrDict] = None + cache_from: Optional[List[str]] = None + cache_to: Optional[List[str]] = None + no_cache: Optional[bool] = None + network: Optional[str] = None + pull: Optional[bool] = None + target: Optional[str] = None + shm_size: Optional[Union[int, str]] = None + extra_hosts: Optional[ListOrDict] = None + isolation: Optional[str] = None + secrets: Optional[ServiceConfigOrSecret] = None + tags: Optional[List[str]] = None + + +class BlkioConfig(BaseModel): + class Config: + extra = Extra.forbid + + device_read_bps: Optional[List[BlkioLimit]] = None + device_read_iops: Optional[List[BlkioLimit]] = None + device_write_bps: Optional[List[BlkioLimit]] = None + device_write_iops: Optional[List[BlkioLimit]] = None + weight: Optional[int] = None + weight_device: Optional[List[BlkioWeight]] = None + + +class ServiceNetwork(BaseModel): + class Config: + extra = Extra.forbid + + aliases: Optional[ListOfStrings] = None + ipv4_address: Optional[str] = None + ipv6_address: Optional[str] = None + link_local_ips: Optional[ListOfStrings] = None + priority: Optional[float] = None + + +class Device(BaseModel): + class Config: + extra = Extra.forbid + + capabilities: Optional[ListOfStrings] = None + count: Optional[Union[str, int]] = None + device_ids: Optional[ListOfStrings] = None + driver: Optional[str] = None + options: Optional[ListOrDict] = None + + +class Devices(BaseModel): + __root__: List[Device] + + +class Network(BaseModel): + class Config: + extra = Extra.forbid + + name: Optional[str] = None + driver: Optional[str] = None + driver_opts: Optional[Dict[constr(regex=r"^.+$"), Union[str, float]]] = None # type: ignore # noqa: F722 + ipam: Optional[Ipam] = None + external: Optional[ExternalNetwork] = None + internal: Optional[bool] = None + enable_ipv6: Optional[bool] = None + attachable: Optional[bool] = None + labels: Optional[ListOrDict] = None + + +class Volume(BaseModel): + class Config: + extra = Extra.forbid + + name: Optional[str] = None + driver: Optional[str] = None + driver_opts: Optional[Dict[constr(regex=r"^.+$"), Union[str, float]]] = None # type: ignore # noqa: F722 + external: Optional[ExternalVolume] = None + labels: Optional[ListOrDict] = None + + +class Secret(BaseModel): + class Config: + extra = Extra.forbid + + name: Optional[str] = None + file: Optional[str] = None + external: Optional[ExternalSecret] = None + labels: Optional[ListOrDict] = None + driver: Optional[str] = None + driver_opts: Optional[Dict[constr(regex=r"^.+$"), Union[str, float]]] = None # type: ignore # noqa: F722 + template_driver: Optional[str] = None + + +class Config(BaseModel): + class Config: + extra = Extra.forbid + + name: Optional[str] = None + file: Optional[str] = None + external: Optional[ExternalConfig] = None + labels: Optional[ListOrDict] = None + template_driver: Optional[str] = None + + +class StringOrList(BaseModel): + __root__: Union[str, ListOfStrings] + + +class Reservations(BaseModel): + class Config: + extra = Extra.forbid + + cpus: Optional[Union[float, str]] = None + memory: Optional[str] = None + generic_resources: Optional[GenericResources] = None + devices: Optional[Devices] = None + + +class Resources(BaseModel): + class Config: + extra = Extra.forbid + + limits: Optional[Limits] = None + reservations: Optional[Reservations] = None + + +class Deployment(BaseModel): + class Config: + extra = Extra.forbid + + mode: Optional[str] = None + endpoint_mode: Optional[str] = None + replicas: Optional[int] = None + labels: Optional[ListOrDict] = None + rollback_config: Optional[RollbackConfig] = None + update_config: Optional[UpdateConfig] = None + resources: Optional[Resources] = None + restart_policy: Optional[RestartPolicy] = None + placement: Optional[Placement] = None + + +class Service(BaseModel): + class Config: + extra = Extra.forbid + + deploy: Optional[Deployment] = None + build: Optional[Union[str, BuildItem]] = None + blkio_config: Optional[BlkioConfig] = None + cap_add: Optional[List[str]] = Field(None, unique_items=True) + cap_drop: Optional[List[str]] = Field(None, unique_items=True) + cgroup_parent: Optional[str] = None + command: Optional[Union[str, List[str]]] = None + configs: Optional[ServiceConfigOrSecret] = None + container_name: Optional[str] = None + cpu_count: Optional[conint(ge=0)] = None # type: ignore + cpu_percent: Optional[conint(ge=0, le=100)] = None # type: ignore + cpu_shares: Optional[Union[float, str]] = None + cpu_quota: Optional[Union[float, str]] = None + cpu_period: Optional[Union[float, str]] = None + cpu_rt_period: Optional[Union[float, str]] = None + cpu_rt_runtime: Optional[Union[float, str]] = None + cpus: Optional[Union[float, str]] = None + cpuset: Optional[str] = None + credential_spec: Optional[CredentialSpec] = None + depends_on: Optional[Union[ListOfStrings, Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), DependsOn]]] = None # type: ignore # noqa: F722, E501 + device_cgroup_rules: Optional[ListOfStrings] = None + devices: Optional[List[str]] = Field(None, unique_items=True) + dns: Optional[StringOrList] = None + dns_opt: Optional[List[str]] = Field(None, unique_items=True) + dns_search: Optional[StringOrList] = None + domainname: Optional[str] = None + entrypoint: Optional[Union[str, List[str]]] = None + env_file: Optional[StringOrList] = None + environment: Optional[ListOrDict] = None + expose: Optional[List[Union[str, float]]] = Field(None, unique_items=True) + extends: Optional[Union[str, Extend]] = None + external_links: Optional[List[str]] = Field(None, unique_items=True) + extra_hosts: Optional[ListOrDict] = None + group_add: Optional[List[Union[str, float]]] = Field(None, unique_items=True) + healthcheck: Optional[Healthcheck] = None + hostname: Optional[str] = None + image: Optional[str] = None + init: Optional[bool] = None + ipc: Optional[str] = None + isolation: Optional[str] = None + labels: Optional[ListOrDict] = None + links: Optional[List[str]] = Field(None, unique_items=True) + logging: Optional[Logging] = None + mac_address: Optional[str] = None + mem_limit: Optional[Union[float, str]] = None + mem_reservation: Optional[Union[str, int]] = None + mem_swappiness: Optional[int] = None + memswap_limit: Optional[Union[float, str]] = None + network_mode: Optional[str] = None + networks: Optional[Union[ListOfStrings, Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Optional[ServiceNetwork]]]] = None # type: ignore # noqa: F722, E501 + oom_kill_disable: Optional[bool] = None + oom_score_adj: Optional[conint(ge=-1000, le=1000)] = None # type: ignore + pid: Optional[Optional[str]] = None + pids_limit: Optional[Union[float, str]] = None + platform: Optional[str] = None + ports: Optional[List[Union[float, str, Port]]] = Field(None, unique_items=True) + privileged: Optional[bool] = None + profiles: Optional[ListOfStrings] = None + pull_policy: Optional[PullPolicy] = None + read_only: Optional[bool] = None + restart: Optional[str] = None + runtime: Optional[str] = None + scale: Optional[int] = None + security_opt: Optional[List[str]] = Field(None, unique_items=True) + shm_size: Optional[Union[float, str]] = None + secrets: Optional[ServiceConfigOrSecret] = None + sysctls: Optional[ListOrDict] = None + stdin_open: Optional[bool] = None + stop_grace_period: Optional[str] = None + stop_signal: Optional[str] = None + storage_opt: Optional[Dict[str, Any]] = None + tmpfs: Optional[StringOrList] = None + tty: Optional[bool] = None + ulimits: Optional[Dict[constr(regex=r"^[a-z]+$"), Union[int, Ulimit]]] = None # type: ignore # noqa: F722 + user: Optional[str] = None + userns_mode: Optional[str] = None + volumes: Optional[List[Union[str, ServiceVolume]]] = Field(None, unique_items=True) + volumes_from: Optional[List[str]] = Field(None, unique_items=True) + working_dir: Optional[str] = None + + +class ComposeSpecification(BaseModel): + class Config: + extra = Extra.forbid + + version: Optional[str] = Field(None, description="declared for backward compatibility, ignored.") + name: Optional[str] = Field( + None, + description="define the Compose project name, until user defines one explicitly.", + ) + services: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Service]] = None # type: ignore # noqa: F722 + networks: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Network]] = None # type: ignore # noqa: F722 + volumes: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Volume]] = None # type: ignore # noqa: F722 + secrets: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Secret]] = None # type: ignore # noqa: F722 + configs: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Config]] = None # type: ignore # noqa: F722 diff --git a/pyproject.toml b/pyproject.toml index 4ba3c53..f71b239 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,3 +30,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] cpv = "compose_viz.cli:start_cli" + +[tool.coverage.run] +source = ["compose_viz"] +omit = ["compose_viz/spec/*"] diff --git a/tests/test_extends.py b/tests/test_extends.py index ad7e4c5..d54cd5d 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -1,6 +1,6 @@ import pytest -from compose_viz.extends import Extends +from compose_viz.models.extends import Extends def test_extend_init_normal() -> None: diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index f384789..2dfd0d8 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -1,11 +1,11 @@ import pytest -from compose_viz.compose import Compose -from compose_viz.extends import Extends +from compose_viz.models.compose import Compose +from compose_viz.models.extends import Extends +from compose_viz.models.port import Port, Protocol +from compose_viz.models.service import Service +from compose_viz.models.volume import Volume, VolumeType from compose_viz.parser import Parser -from compose_viz.port import Port, Protocol -from compose_viz.service import Service -from compose_viz.volume import Volume, VolumeType @pytest.mark.parametrize( diff --git a/tests/test_port.py b/tests/test_port.py index 159c92c..6a2afcb 100644 --- a/tests/test_port.py +++ b/tests/test_port.py @@ -1,4 +1,4 @@ -from compose_viz.port import Port, Protocol +from compose_viz.models.port import Port, Protocol def test_port_init_normal() -> None: diff --git a/tests/test_volume.py b/tests/test_volume.py index b3651ec..5e9cf9c 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -1,4 +1,4 @@ -from compose_viz.volume import Volume, VolumeType +from compose_viz.models.volume import Volume, VolumeType def test_volume_init_normal() -> None: From b2b5cd49ba7b8a56bdb29af948ae45220b20dcc3 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 16:15:57 +0800 Subject: [PATCH 2/8] feat: add pydantic_yaml to parse yaml file --- compose_viz/spec/compose_spec.py | 103 ++++++++-------- poetry.lock | 206 ++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 258 insertions(+), 52 deletions(-) diff --git a/compose_viz/spec/compose_spec.py b/compose_viz/spec/compose_spec.py index 0880ac6..24caa0b 100644 --- a/compose_viz/spec/compose_spec.py +++ b/compose_viz/spec/compose_spec.py @@ -7,10 +7,11 @@ from __future__ import annotations from enum import Enum from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, Extra, Field, conint, constr +from pydantic import Extra, Field, conint, constr +from pydantic_yaml import YamlModel -class CredentialSpec(BaseModel): +class CredentialSpec(YamlModel): class Config: extra = Extra.forbid @@ -25,14 +26,14 @@ class Condition(Enum): service_completed_successfully = "service_completed_successfully" -class DependsOn(BaseModel): +class DependsOn(YamlModel): class Config: extra = Extra.forbid condition: Condition -class Extend(BaseModel): +class Extend(YamlModel): class Config: extra = Extra.forbid @@ -40,7 +41,7 @@ class Extend(BaseModel): file: Optional[str] = None -class Logging(BaseModel): +class Logging(YamlModel): class Config: extra = Extra.forbid @@ -48,7 +49,7 @@ class Logging(BaseModel): options: Optional[Dict[constr(regex=r"^.+$"), Optional[Union[str, float]]]] = None # type: ignore # noqa: F722 -class Port(BaseModel): +class Port(YamlModel): class Config: extra = Extra.forbid @@ -67,7 +68,7 @@ class PullPolicy(Enum): missing = "missing" -class Ulimit(BaseModel): +class Ulimit(YamlModel): class Config: extra = Extra.forbid @@ -80,7 +81,7 @@ class Selinux(Enum): Z = "Z" -class Bind(BaseModel): +class Bind(YamlModel): class Config: extra = Extra.forbid @@ -89,14 +90,14 @@ class Bind(BaseModel): selinux: Optional[Selinux] = None -class AdditionalVolumeOption(BaseModel): +class AdditionalVolumeOption(YamlModel): class Config: extra = Extra.forbid nocopy: Optional[bool] = None -class Tmpfs(BaseModel): +class Tmpfs(YamlModel): class Config: extra = Extra.forbid @@ -104,7 +105,7 @@ class Tmpfs(BaseModel): mode: Optional[float] = None -class ServiceVolume(BaseModel): +class ServiceVolume(YamlModel): class Config: extra = Extra.forbid @@ -118,7 +119,7 @@ class ServiceVolume(BaseModel): tmpfs: Optional[Tmpfs] = None -class Healthcheck(BaseModel): +class Healthcheck(YamlModel): class Config: extra = Extra.forbid @@ -135,7 +136,7 @@ class Order(Enum): stop_first = "stop-first" -class RollbackConfig(BaseModel): +class RollbackConfig(YamlModel): class Config: extra = Extra.forbid @@ -152,7 +153,7 @@ class ConfigOrder(Enum): stop_first = "stop-first" -class UpdateConfig(BaseModel): +class UpdateConfig(YamlModel): class Config: extra = Extra.forbid @@ -164,7 +165,7 @@ class UpdateConfig(BaseModel): order: Optional[ConfigOrder] = None -class Limits(BaseModel): +class Limits(YamlModel): class Config: extra = Extra.forbid @@ -173,7 +174,7 @@ class Limits(BaseModel): pids: Optional[int] = None -class RestartPolicy(BaseModel): +class RestartPolicy(YamlModel): class Config: extra = Extra.forbid @@ -183,14 +184,14 @@ class RestartPolicy(BaseModel): window: Optional[str] = None -class Preference(BaseModel): +class Preference(YamlModel): class Config: extra = Extra.forbid spread: Optional[str] = None -class Placement(BaseModel): +class Placement(YamlModel): class Config: extra = Extra.forbid @@ -199,7 +200,7 @@ class Placement(BaseModel): max_replicas_per_node: Optional[int] = None -class DiscreteResourceSpec(BaseModel): +class DiscreteResourceSpec(YamlModel): class Config: extra = Extra.forbid @@ -207,18 +208,18 @@ class DiscreteResourceSpec(BaseModel): value: Optional[float] = None -class GenericResource(BaseModel): +class GenericResource(YamlModel): class Config: extra = Extra.forbid discrete_resource_spec: Optional[DiscreteResourceSpec] = None -class GenericResources(BaseModel): +class GenericResources(YamlModel): __root__: List[GenericResource] -class ConfigItem(BaseModel): +class ConfigItem(YamlModel): class Config: extra = Extra.forbid @@ -228,7 +229,7 @@ class ConfigItem(BaseModel): aux_addresses: Optional[Dict[constr(regex=r"^.+$"), str]] = None # type: ignore # noqa: F722 -class Ipam(BaseModel): +class Ipam(YamlModel): class Config: extra = Extra.forbid @@ -237,37 +238,37 @@ class Ipam(BaseModel): options: Optional[Dict[constr(regex=r"^.+$"), str]] = None # type: ignore # noqa: F722 -class ExternalNetwork(BaseModel): +class ExternalNetwork(YamlModel): class Config: extra = Extra.forbid name: Optional[str] = None -class ExternalVolume(BaseModel): +class ExternalVolume(YamlModel): class Config: extra = Extra.forbid name: Optional[str] = None -class ExternalSecret(BaseModel): +class ExternalSecret(YamlModel): name: Optional[str] = None -class ExternalConfig(BaseModel): +class ExternalConfig(YamlModel): name: Optional[str] = None -class ListOfStrings(BaseModel): +class ListOfStrings(YamlModel): __root__: List[str] = Field(..., unique_items=True) -class ListOrDict(BaseModel): +class ListOrDict(YamlModel): __root__: Union[Dict[constr(regex=r".+"), Optional[Union[str, float, bool]]], List[str]] # type: ignore # noqa: F722, E501 -class BlkioLimit(BaseModel): +class BlkioLimit(YamlModel): class Config: extra = Extra.forbid @@ -275,7 +276,7 @@ class BlkioLimit(BaseModel): rate: Optional[Union[int, str]] = None -class BlkioWeight(BaseModel): +class BlkioWeight(YamlModel): class Config: extra = Extra.forbid @@ -283,7 +284,7 @@ class BlkioWeight(BaseModel): weight: Optional[int] = None -class ServiceConfigOrSecretItem(BaseModel): +class ServiceConfigOrSecretItem(YamlModel): class Config: extra = Extra.forbid @@ -294,15 +295,15 @@ class ServiceConfigOrSecretItem(BaseModel): mode: Optional[float] = None -class ServiceConfigOrSecret(BaseModel): +class ServiceConfigOrSecret(YamlModel): __root__: List[Union[str, ServiceConfigOrSecretItem]] -class Constraints(BaseModel): +class Constraints(YamlModel): __root__: Any -class BuildItem(BaseModel): +class BuildItem(YamlModel): class Config: extra = Extra.forbid @@ -324,7 +325,7 @@ class BuildItem(BaseModel): tags: Optional[List[str]] = None -class BlkioConfig(BaseModel): +class BlkioConfig(YamlModel): class Config: extra = Extra.forbid @@ -336,7 +337,7 @@ class BlkioConfig(BaseModel): weight_device: Optional[List[BlkioWeight]] = None -class ServiceNetwork(BaseModel): +class ServiceNetwork(YamlModel): class Config: extra = Extra.forbid @@ -347,7 +348,7 @@ class ServiceNetwork(BaseModel): priority: Optional[float] = None -class Device(BaseModel): +class Device(YamlModel): class Config: extra = Extra.forbid @@ -358,11 +359,11 @@ class Device(BaseModel): options: Optional[ListOrDict] = None -class Devices(BaseModel): +class Devices(YamlModel): __root__: List[Device] -class Network(BaseModel): +class Network(YamlModel): class Config: extra = Extra.forbid @@ -377,7 +378,7 @@ class Network(BaseModel): labels: Optional[ListOrDict] = None -class Volume(BaseModel): +class Volume(YamlModel): class Config: extra = Extra.forbid @@ -388,7 +389,7 @@ class Volume(BaseModel): labels: Optional[ListOrDict] = None -class Secret(BaseModel): +class Secret(YamlModel): class Config: extra = Extra.forbid @@ -401,7 +402,7 @@ class Secret(BaseModel): template_driver: Optional[str] = None -class Config(BaseModel): +class Config(YamlModel): class Config: extra = Extra.forbid @@ -412,11 +413,11 @@ class Config(BaseModel): template_driver: Optional[str] = None -class StringOrList(BaseModel): +class StringOrList(YamlModel): __root__: Union[str, ListOfStrings] -class Reservations(BaseModel): +class Reservations(YamlModel): class Config: extra = Extra.forbid @@ -426,7 +427,7 @@ class Reservations(BaseModel): devices: Optional[Devices] = None -class Resources(BaseModel): +class Resources(YamlModel): class Config: extra = Extra.forbid @@ -434,7 +435,7 @@ class Resources(BaseModel): reservations: Optional[Reservations] = None -class Deployment(BaseModel): +class Deployment(YamlModel): class Config: extra = Extra.forbid @@ -449,7 +450,7 @@ class Deployment(BaseModel): placement: Optional[Placement] = None -class Service(BaseModel): +class Service(YamlModel): class Config: extra = Extra.forbid @@ -534,7 +535,7 @@ class Service(BaseModel): working_dir: Optional[str] = None -class ComposeSpecification(BaseModel): +class ComposeSpecification(YamlModel): class Config: extra = Extra.forbid @@ -544,7 +545,7 @@ class ComposeSpecification(BaseModel): description="define the Compose project name, until user defines one explicitly.", ) services: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Service]] = None # type: ignore # noqa: F722 - networks: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Network]] = None # type: ignore # noqa: F722 - volumes: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Volume]] = None # type: ignore # noqa: F722 + networks: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Optional[Network]]] = None # type: ignore # noqa: F722 + volumes: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Optional[Volume]]] = None # type: ignore # noqa: F722 secrets: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Secret]] = None # type: ignore # noqa: F722 configs: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Config]] = None # type: ignore # noqa: F722 diff --git a/poetry.lock b/poetry.lock index 6043af8..98e1ba4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,6 +61,20 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] + [[package]] name = "distlib" version = "0.3.4" @@ -180,6 +194,41 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pydantic" +version = "1.9.1" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pydantic-yaml" +version = "0.6.3" +description = "\"Adds some YAML functionality to the excellent `pydantic` library.\"" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +deprecated = ">=1.2.5,<1.3.0" +pydantic = ">=1.7.4,<2.0" +semver = ">=2.13.0,<4" +types-Deprecated = "*" + +[package.extras] +dev = ["black", "flake8", "bump2version", "pytest", "mypy"] +docs = ["mkdocs", "mkdocs-material", "mkdocstrings", "pymdown-extensions", "pygments"] +pyyaml = ["pyyaml", "types-pyyaml"] +ruamel = ["ruamel.yaml (>=0.15,<0.18)"] + [[package]] name = "pyparsing" version = "3.0.8" @@ -258,6 +307,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "six" version = "1.16.0" @@ -299,6 +356,22 @@ dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)"] +[[package]] +name = "types-deprecated" +version = "1.2.8" +description = "Typing stubs for Deprecated" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "virtualenv" version = "20.14.1" @@ -317,10 +390,18 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "e1b68a4c83f398e841e6f38823ea18f3cb27b0b251689d0398ae39c03ddc4a47" +content-hash = "e7a6c60bfd8d85b8dce3e04a4ad0776a106a4ec18483787f93d29a738efd0700" [metadata.files] atomicwrites = [ @@ -386,6 +467,10 @@ coverage = [ {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, ] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, @@ -430,6 +515,47 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pydantic = [ + {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"}, + {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"}, + {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"}, + {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"}, + {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"}, + {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"}, + {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"}, + {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"}, + {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"}, + {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"}, + {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"}, +] +pydantic-yaml = [ + {file = "pydantic_yaml-0.6.3-py3-none-any.whl", hash = "sha256:aaa3e9c55eeebc203dacd461ff635932d10a440bfda7f77f596351094c85322b"}, + {file = "pydantic_yaml-0.6.3.tar.gz", hash = "sha256:8bec8b6eee9889073b5461075d8b85efb00ba43bd34d5fb331fa394ec5a3b46f"}, +] pyparsing = [ {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, @@ -508,6 +634,10 @@ pyyaml = [ {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -524,7 +654,81 @@ typer = [ {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"}, {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"}, ] +types-deprecated = [ + {file = "types-Deprecated-1.2.8.tar.gz", hash = "sha256:62e1b773cafaec26e5e7c85f6f476f65aba1b5cb0857b0cc71d1eeb8c576e6a2"}, + {file = "types_Deprecated-1.2.8-py3-none-any.whl", hash = "sha256:5adf04a18d1d0ae7d82b56cd07c2386c6639026ac5f7a9eb47f904d934697430"}, +] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] virtualenv = [ {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, ] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] diff --git a/pyproject.toml b/pyproject.toml index f71b239..926de26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ typer = "^0.4.1" PyYAML = "^6.0" graphviz = "^0.20" "ruamel.yaml" = "^0.17.21" +pydantic-yaml = "^0.6.3" [tool.poetry.dev-dependencies] pytest = "^7.1.2" From d4a9c99f79066f9d6c361dc8f2c1682bff57bfc0 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 17:29:07 +0800 Subject: [PATCH 3/8] feat: imeplement parser using compose-spec schema --- compose_viz/parser.py | 273 +++++++++++++++++------------------------- 1 file changed, 112 insertions(+), 161 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 96e2667..50627f3 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,8 +1,7 @@ import re -from typing import Dict, List, Optional - -from ruamel.yaml import YAML +from typing import List, Optional +import compose_viz.spec.compose_spec as spec from compose_viz.models.compose import Compose, Service from compose_viz.models.extends import Extends from compose_viz.models.port import Port, Protocol @@ -14,182 +13,124 @@ class Parser: pass def parse(self, file_path: str) -> Compose: - # load the yaml file - with open(file_path, "r") as f: - try: - yaml = YAML(typ="safe", pure=True) - yaml_data = yaml.load(f) - except Exception as e: - raise RuntimeError(f"Error parsing file '{file_path}': {e}") - # validate the yaml file - if not yaml_data: - raise RuntimeError("Empty yaml file, aborting.") - if not yaml_data.get("services"): - raise RuntimeError("No services found, aborting.") - - # parse services data into Service objects - services = self.parse_service_data(yaml_data["services"]) - - # create Compose object - compose = Compose(services) - - return compose - - def parse_service_data(self, services_yaml_data: Dict[str, dict]) -> List[Service]: + compose_data: spec.ComposeSpecification = spec.ComposeSpecification.parse_file(file_path) services: List[Service] = [] - for service, service_name in zip(services_yaml_data.values(), services_yaml_data.keys()): + + assert compose_data.services is not None, "No services found, aborting." + + for service_name, service_data in compose_data.services.items(): + service_name = str(service_name) service_image: Optional[str] = None - if service.get("build"): - if type(service["build"]) is str: - service_image = f"build from '{service['build']}'" - elif type(service["build"]) is dict: - if service["build"].get("context") and service["build"].get("dockerfile"): + if service_data.build is not None: + if type(service_data.build) is str: + service_image = f"build from '{service_data.build}'" + elif type(service_data.build) is spec.BuildItem: + if service_data.build.context is not None and service_data.build.dockerfile is not None: service_image = ( - f"build from '{service['build']['context']}' using '{service['build']['dockerfile']}'" + f"build from '{service_data.build.context}' using '{service_data.build.dockerfile}'" ) - elif service["build"].get("context"): - service_image = f"build from '{service['build']['context']}'" - if service.get("image"): - if service_image: - service_image += ", image: " + service["image"] + elif service_data.build.context is not None: + service_image = f"build from '{service_data.build.context}'" + if service_data.image is not None: + if service_image is not None: + service_image += ", image: " + service_data.image else: - service_image = service["image"] + service_image = service_data.image service_networks: List[str] = [] - if service.get("networks"): - if type(service["networks"]) is list: - service_networks = service["networks"] - elif type(service["networks"]) is dict: - service_networks = list(service["networks"].keys()) + if service_data.networks is not None: + if type(service_data.networks) is spec.ListOfStrings: + service_networks = service_data.networks.__root__ + elif type(service_data.networks) is dict: + service_networks = list(service_data.networks.keys()) service_extends: Optional[Extends] = None - if service.get("extends"): - assert type(service["extends"]) is dict, "Invalid extends format, aborting." - assert service["extends"]["service"], "Missing extends service, aborting." - extend_service_name = str(service["extends"]["service"]) - - extend_from_file: Optional[str] = None - if service["extends"].get("file"): - assert service["extends"]["file"], "Missing extends file, aborting." - extend_from_file = str(service["extends"]["file"]) - - service_extends = Extends(service_name=extend_service_name, from_file=extend_from_file) + if service_data.extends is not None: + if type(service_data.extends) is str: + service_extends = Extends(service_name=service_data.extends) + elif type(service_data.extends) is spec.Extend: + service_extends = Extends( + service_name=service_data.extends.service, from_file=service_data.extends.file + ) service_ports: List[Port] = [] - if service.get("ports"): - assert type(service["ports"]) is list - for port_data in service["ports"]: - if type(port_data) is dict: - # define a nested function to limit variable scope - def long_syntax(): - assert type(port_data) is dict - assert port_data["target"] + if service_data.ports is not None: + for port_data in service_data.ports: + host_ip: Optional[str] = None + host_port: Optional[str] = None + container_port: Optional[str] = None + protocol: Optional[str] = None - container_port: str = str(port_data["target"]) - host_port: str = "" - protocol: Protocol = Protocol.any + if type(port_data) is str: + regex = r"(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?((?P\d+(\-\d+)?):)?((?P\d+(\-\d+)?))?(/(?P\w+))?" # noqa: E501 + match = re.match(regex, port_data) - if port_data.get("published"): - host_port = str(port_data["published"]) - else: + if match: + host_ip = match.group("host_ip") + host_port = match.group("host_port") + container_port = match.group("container_port") + protocol = match.group("protocol") + + assert container_port, "Invalid port format, aborting." + + if container_port and not host_port: host_port = container_port - if port_data.get("host_ip"): - host_ip = str(port_data["host_ip"]) - host_port = f"{host_ip}:{host_port}" + if host_ip: + host_port = f"{host_ip}{host_port}" else: host_port = f"0.0.0.0:{host_port}" + elif type(port_data) is spec.Port: + assert port_data.target is not None, "Invalid port format, aborting." - if port_data.get("protocol"): - protocol = Protocol[str(port_data["protocol"])] + if type(port_data.published) is int: + host_port = str(port_data.published) + elif type(port_data.published) is str: + host_port = port_data.published - assert host_port, "Error parsing port, aborting." + if type(port_data.target) is int: + container_port = str(port_data.target) + elif type(port_data.target) is str: + container_port = port_data.target - service_ports.append( - Port( - host_port=host_port, - container_port=container_port, - protocol=protocol, - ) - ) + host_ip = port_data.host_ip + protocol = port_data.protocol - long_syntax() - elif type(port_data) is str: - # ports that needs to parse using regex: - # - "3000" - # - "3000-3005" - # - "8000:8000" - # - "9090-9091:8080-8081" - # - "49100:22" - # - "127.0.0.1:8001:8001" - # - "127.0.0.1:5000-5010:5000-5010" - # - "6060:6060/udp" + if container_port and not host_port: + host_port = container_port - def short_syntax(): - assert type(port_data) is str - regex = r"(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?((?P\d+(\-\d+)?):)?((?P\d+(\-\d+)?))?(/(?P\w+))?" # noqa: E501 - match = re.match(regex, port_data) - if match: - host_ip: Optional[str] = match.group("host_ip") - host_port: Optional[str] = match.group("host_port") - container_port: Optional[str] = match.group("container_port") - protocol: Optional[str] = match.group("protocol") + if host_ip: + host_port = f"{host_ip}{host_port}" + else: + host_port = f"0.0.0.0:{host_port}" - assert container_port, "Invalid port format, aborting." + assert host_port is not None, "Error while parsing port, aborting." + assert container_port is not None, "Error while parsing port, aborting." - if container_port and not host_port: - host_port = container_port + if protocol is None: + protocol = "any" - if host_ip: - host_port = f"{host_ip}{host_port}" - else: - host_port = f"0.0.0.0:{host_port}" - - assert host_port, "Error while parsing port, aborting." - - if protocol: - service_ports.append( - Port( - host_port=host_port, - container_port=container_port, - protocol=Protocol[protocol], - ) - ) - else: - service_ports.append( - Port( - host_port=host_port, - container_port=container_port, - ) - ) - - short_syntax() + service_ports.append( + Port( + host_port=host_port, + container_port=container_port, + protocol=Protocol[protocol], + ) + ) service_depends_on: List[str] = [] - if service.get("depends_on"): - if type(service["depends_on"]) is list: - for depends_on in service["depends_on"]: + if service_data.depends_on is not None: + if type(service_data.depends_on) is spec.ListOfStrings: + service_depends_on = service_data.depends_on.__root__ + elif type(service_data.depends_on) is dict: + for depends_on in service_data.depends_on.keys(): service_depends_on.append(str(depends_on)) - elif type(service["depends_on"]) is dict: - service_depends_on = list(service["depends_on"].keys()) service_volumes: List[Volume] = [] - if service.get("volumes"): - assert type(service["volumes"]) is list - for volume_data in service["volumes"]: - if type(volume_data) is dict: - assert volume_data["source"] and volume_data["target"], "Invalid volume input, aborting." - - volume_source: str = str(volume_data["source"]) - volume_target: str = str(volume_data["target"]) - volume_type: VolumeType = VolumeType.volume - - if volume_data.get("type"): - volume_type = VolumeType[str(volume_data["type"])] - - service_volumes.append(Volume(source=volume_source, target=volume_target, type=volume_type)) - elif type(volume_data) is str: + if service_data.volumes is not None: + for volume_data in service_data.volumes: + if type(volume_data) is str: assert ":" in volume_data, "Invalid volume input, aborting." spilt_data = volume_data.split(":") @@ -197,12 +138,31 @@ class Parser: service_volumes.append(Volume(source=spilt_data[0], target=spilt_data[1])) elif len(spilt_data) == 3: service_volumes.append( - Volume(source=spilt_data[0], target=spilt_data[1], access_mode=spilt_data[2]) + Volume( + source=spilt_data[0], + target=spilt_data[1], + access_mode=spilt_data[2], + ) ) + elif type(volume_data) is spec.ServiceVolume: + assert volume_data.target is not None, "Invalid volume input, aborting." + + if volume_data.source is None: + volume_data.source = volume_data.target + + assert volume_data.source is not None + + service_volumes.append( + Volume( + source=volume_data.source, + target=volume_data.target, + type=VolumeType[volume_data.type], + ) + ) service_links: List[str] = [] - if service.get("links"): - service_links = service["links"] + if service_data.links is not None: + service_links = service_data.links services.append( Service( @@ -216,14 +176,5 @@ class Parser: links=service_links, ) ) - # Service print debug - # print("--------------------") - # print("Service name: {}".format(service_name)) - # print("image: {}".format(service_image)) - # print("networks: {}".format(service_networks)) - # print("image: {}".format(service_image)) - # print("extends: {}".format(service_extends)) - # print("ports: {}".format(service_ports)) - # print("depends: {}".format(service_depends_on)) - return services + return Compose(services=services) From ad5bad8d2074a8d709f26b6d40d3471d56fdda63 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 17:37:39 +0800 Subject: [PATCH 4/8] fix: port parsing --- compose_viz/parser.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 50627f3..9ae4e86 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -62,7 +62,10 @@ class Parser: container_port: Optional[str] = None protocol: Optional[str] = None - if type(port_data) is str: + if type(port_data) is float: + container_port = str(int(port_data)) + host_port = f"0.0.0.0:{container_port}" + elif type(port_data) is str: regex = r"(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?((?P\d+(\-\d+)?):)?((?P\d+(\-\d+)?))?(/(?P\w+))?" # noqa: E501 match = re.match(regex, port_data) @@ -74,10 +77,10 @@ class Parser: assert container_port, "Invalid port format, aborting." - if container_port and not host_port: + if container_port is not None and host_port is None: host_port = container_port - if host_ip: + if host_ip is not None: host_port = f"{host_ip}{host_port}" else: host_port = f"0.0.0.0:{host_port}" @@ -97,11 +100,11 @@ class Parser: host_ip = port_data.host_ip protocol = port_data.protocol - if container_port and not host_port: + if container_port is not None and host_port is None: host_port = container_port - if host_ip: - host_port = f"{host_ip}{host_port}" + if host_ip is not None: + host_port = f"{host_ip}:{host_port}" else: host_port = f"0.0.0.0:{host_port}" From 2e50d1b4d1a5b482fda9e0a90e689758efe79b0d Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 19:56:15 +0800 Subject: [PATCH 5/8] test: adjust error format --- tests/test_parser.py | 4 ++-- tests/ymls/others/no-services.yml | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 0e3d33f..cf996c1 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -9,10 +9,10 @@ def test_parser_error_parsing_file() -> None: def test_parser_invalid_yaml() -> None: - with pytest.raises(RuntimeError, match=r"Empty yaml file, aborting."): + with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/ymls/others/empty.yml'.*"): Parser().parse("tests/ymls/others/empty.yml") def test_parser_no_services_found() -> None: - with pytest.raises(RuntimeError, match=r"No services found, aborting."): + with pytest.raises(AssertionError, match=r"No services found, aborting."): Parser().parse("tests/ymls/others/no-services.yml") diff --git a/tests/ymls/others/no-services.yml b/tests/ymls/others/no-services.yml index a0bf4a1..30200b0 100644 --- a/tests/ymls/others/no-services.yml +++ b/tests/ymls/others/no-services.yml @@ -1,3 +1 @@ -what-is-this: - - "a yaml file without services" - - "test purpose" +version: "A docker-compose file without services." From 83e4f3422fd8140f3eb98572375153dae09e5612 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 19:56:30 +0800 Subject: [PATCH 6/8] chore: implement new error format --- compose_viz/parser.py | 10 +++++++++- tests/test_parser.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 9ae4e86..4c212c7 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,6 +1,8 @@ import re from typing import List, Optional +from pydantic import ValidationError + import compose_viz.spec.compose_spec as spec from compose_viz.models.compose import Compose, Service from compose_viz.models.extends import Extends @@ -13,7 +15,13 @@ class Parser: pass def parse(self, file_path: str) -> Compose: - compose_data: spec.ComposeSpecification = spec.ComposeSpecification.parse_file(file_path) + compose_data: spec.ComposeSpecification + + try: + compose_data = spec.ComposeSpecification.parse_file(file_path) + except ValidationError as e: + raise RuntimeError(f"Error parsing file '{file_path}': {e}") + services: List[Service] = [] assert compose_data.services is not None, "No services found, aborting." diff --git a/tests/test_parser.py b/tests/test_parser.py index cf996c1..c7fa2ef 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,12 +3,12 @@ import pytest from compose_viz.parser import Parser -def test_parser_error_parsing_file() -> None: +def test_parser_invalid_yaml() -> None: with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/ymls/others/invalid.yml'.*"): Parser().parse("tests/ymls/others/invalid.yml") -def test_parser_invalid_yaml() -> None: +def test_parser_empty_yaml() -> None: with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/ymls/others/empty.yml'.*"): Parser().parse("tests/ymls/others/empty.yml") From eb024311ea7e86c89fa131d8c2477dc034624a85 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 20:26:44 +0800 Subject: [PATCH 7/8] test: add tmpfs test case --- tests/test_parse_file.py | 11 +++++++++++ tests/ymls/volumes/docker-compose.yml | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 2dfd0d8..7bf3cdc 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -227,6 +227,17 @@ from compose_viz.parser import Parser ), ], ), + Service( + name="tmp", + image="awesome/nginx", + volumes=[ + Volume( + source="/app", + target="/app", + type=VolumeType.tmpfs, + ), + ], + ), ], ), ), diff --git a/tests/ymls/volumes/docker-compose.yml b/tests/ymls/volumes/docker-compose.yml index 4edb77c..71eed34 100644 --- a/tests/ymls/volumes/docker-compose.yml +++ b/tests/ymls/volumes/docker-compose.yml @@ -17,6 +17,11 @@ services: service: common volumes: - cli-volume:/var/lib/backup/data:ro + tmp: + image: awesome/nginx + volumes: + - type: tmpfs + target: /app volumes: common-volume: From 73153c980e2092f258797b1d515d6661149ef12d Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Fri, 27 May 2022 20:27:59 +0800 Subject: [PATCH 8/8] fix: remove some unreachable situations --- compose_viz/parser.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 4c212c7..63139d4 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -55,12 +55,12 @@ class Parser: service_extends: Optional[Extends] = None if service_data.extends is not None: - if type(service_data.extends) is str: - service_extends = Extends(service_name=service_data.extends) - elif type(service_data.extends) is spec.Extend: - service_extends = Extends( - service_name=service_data.extends.service, from_file=service_data.extends.file - ) + # https://github.com/compose-spec/compose-spec/blob/master/spec.md#extends + # The value of the extends key MUST be a dictionary. + assert type(service_data.extends) is spec.Extend + service_extends = Extends( + service_name=service_data.extends.service, from_file=service_data.extends.file + ) service_ports: List[Port] = [] if service_data.ports is not None: @@ -95,15 +95,14 @@ class Parser: elif type(port_data) is spec.Port: assert port_data.target is not None, "Invalid port format, aborting." - if type(port_data.published) is int: - host_port = str(port_data.published) - elif type(port_data.published) is str: + # ruamel.yaml does not parse port as int + assert type(port_data.published) is not int + + if type(port_data.published) is str: host_port = port_data.published if type(port_data.target) is int: container_port = str(port_data.target) - elif type(port_data.target) is str: - container_port = port_data.target host_ip = port_data.host_ip protocol = port_data.protocol @@ -158,6 +157,8 @@ class Parser: elif type(volume_data) is spec.ServiceVolume: assert volume_data.target is not None, "Invalid volume input, aborting." + # https://github.com/compose-spec/compose-spec/blob/master/spec.md#long-syntax-4 + # `volume_data.source` is not applicable for a tmpfs mount. if volume_data.source is None: volume_data.source = volume_data.target