devservices 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commands/__init__.py +0 -0
- commands/list_dependencies.py +41 -0
- commands/list_services.py +31 -0
- commands/logs.py +50 -0
- commands/start.py +46 -0
- commands/status.py +93 -0
- commands/stop.py +46 -0
- configs/service_config.py +95 -0
- devservices-0.0.1.dist-info/METADATA +13 -0
- devservices-0.0.1.dist-info/RECORD +18 -0
- devservices-0.0.1.dist-info/WHEEL +5 -0
- devservices-0.0.1.dist-info/entry_points.txt +2 -0
- devservices-0.0.1.dist-info/top_level.txt +3 -0
- utils/__init__.py +0 -0
- utils/console.py +67 -0
- utils/devenv.py +21 -0
- utils/docker_compose.py +18 -0
- utils/services.py +61 -0
commands/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from argparse import _SubParsersAction
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from utils.services import find_matching_service
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"list-dependencies", help="List the dependencies of a service"
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"service_name",
|
|
16
|
+
help="Name of the service to list the dependencies of",
|
|
17
|
+
nargs="?",
|
|
18
|
+
default=None,
|
|
19
|
+
)
|
|
20
|
+
parser.set_defaults(func=list_dependencies)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def list_dependencies(args: Namespace) -> None:
|
|
24
|
+
"""List the dependencies of a service."""
|
|
25
|
+
service_name = args.service_name
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
service = find_matching_service(service_name)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(e)
|
|
31
|
+
exit(1)
|
|
32
|
+
|
|
33
|
+
dependencies = service.config.dependencies
|
|
34
|
+
|
|
35
|
+
if not dependencies:
|
|
36
|
+
print(f"No dependencies found for {service.name}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
print(f"Dependencies of {service.name}:")
|
|
40
|
+
for dependency_key, dependency_info in dependencies.items():
|
|
41
|
+
print("-", dependency_key, ":", dependency_info.description)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from argparse import _SubParsersAction
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from utils.devenv import get_coderoot
|
|
8
|
+
from utils.services import get_local_services
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
12
|
+
parser = subparsers.add_parser(
|
|
13
|
+
"list-services", help="List the services installed locally"
|
|
14
|
+
)
|
|
15
|
+
parser.set_defaults(func=list_services)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_services(args: Namespace) -> None:
|
|
19
|
+
"""List the services installed locally."""
|
|
20
|
+
|
|
21
|
+
# Get all of the services installed locally
|
|
22
|
+
coderoot = get_coderoot()
|
|
23
|
+
services = get_local_services(coderoot)
|
|
24
|
+
|
|
25
|
+
if not services:
|
|
26
|
+
print("No services found")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
print("Services installed locally:")
|
|
30
|
+
for service in services:
|
|
31
|
+
print("-", service.name, f"({service.repo_path})")
|
commands/logs.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from argparse import _SubParsersAction
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
from argparse import Namespace
|
|
8
|
+
|
|
9
|
+
from constants import DEVSERVICES_DIR_NAME
|
|
10
|
+
from constants import DOCKER_COMPOSE_FILE_NAME
|
|
11
|
+
from exceptions import DockerComposeError
|
|
12
|
+
from utils.docker_compose import run_docker_compose_command
|
|
13
|
+
from utils.services import find_matching_service
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
17
|
+
parser = subparsers.add_parser("logs", help="View logs for a service")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"service_name",
|
|
20
|
+
help="Name of the service to view logs for",
|
|
21
|
+
nargs="?",
|
|
22
|
+
default=None,
|
|
23
|
+
)
|
|
24
|
+
parser.set_defaults(func=logs)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def logs(args: Namespace) -> None:
|
|
28
|
+
"""View the logs for a specified service."""
|
|
29
|
+
service_name = args.service_name
|
|
30
|
+
try:
|
|
31
|
+
service = find_matching_service(service_name)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
print(e)
|
|
34
|
+
exit(1)
|
|
35
|
+
modes = service.config.modes
|
|
36
|
+
# TODO: allow custom modes to be used
|
|
37
|
+
mode_to_use = "default"
|
|
38
|
+
mode_dependencies = " ".join(modes[mode_to_use])
|
|
39
|
+
service_config_file_path = os.path.join(
|
|
40
|
+
service.repo_path, DEVSERVICES_DIR_NAME, DOCKER_COMPOSE_FILE_NAME
|
|
41
|
+
)
|
|
42
|
+
try:
|
|
43
|
+
logs = run_docker_compose_command(
|
|
44
|
+
f"-f {service_config_file_path} logs {mode_dependencies}"
|
|
45
|
+
)
|
|
46
|
+
except DockerComposeError as dce:
|
|
47
|
+
print(f"Failed to get logs for {service.name}: {dce.stderr}")
|
|
48
|
+
exit(1)
|
|
49
|
+
sys.stdout.write(logs.stdout)
|
|
50
|
+
sys.stdout.flush()
|
commands/start.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from argparse import _SubParsersAction
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from argparse import Namespace
|
|
7
|
+
|
|
8
|
+
from constants import DEVSERVICES_DIR_NAME
|
|
9
|
+
from constants import DOCKER_COMPOSE_FILE_NAME
|
|
10
|
+
from exceptions import DockerComposeError
|
|
11
|
+
from utils.console import Status
|
|
12
|
+
from utils.docker_compose import run_docker_compose_command
|
|
13
|
+
from utils.services import find_matching_service
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
17
|
+
parser = subparsers.add_parser("start", help="Start a service and its dependencies")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"service_name", help="Name of the service to start", nargs="?", default=None
|
|
20
|
+
)
|
|
21
|
+
parser.set_defaults(func=start)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def start(args: Namespace) -> None:
|
|
25
|
+
"""Start a service and its dependencies."""
|
|
26
|
+
service_name = args.service_name
|
|
27
|
+
try:
|
|
28
|
+
service = find_matching_service(service_name)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(e)
|
|
31
|
+
exit(1)
|
|
32
|
+
modes = service.config.modes
|
|
33
|
+
# TODO: allow custom modes to be used
|
|
34
|
+
mode_to_start = "default"
|
|
35
|
+
mode_dependencies = " ".join(modes[mode_to_start])
|
|
36
|
+
service_config_file_path = os.path.join(
|
|
37
|
+
service.repo_path, DEVSERVICES_DIR_NAME, DOCKER_COMPOSE_FILE_NAME
|
|
38
|
+
)
|
|
39
|
+
with Status(f"Starting {service.name}", f"{service.name} started") as status:
|
|
40
|
+
try:
|
|
41
|
+
run_docker_compose_command(
|
|
42
|
+
f"-f {service_config_file_path} up -d {mode_dependencies}"
|
|
43
|
+
)
|
|
44
|
+
except DockerComposeError as dce:
|
|
45
|
+
status.print(f"Failed to start {service.name}: {dce.stderr}")
|
|
46
|
+
exit(1)
|
commands/status.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import _SubParsersAction
|
|
7
|
+
from argparse import ArgumentParser
|
|
8
|
+
from argparse import Namespace
|
|
9
|
+
|
|
10
|
+
from constants import DEVSERVICES_DIR_NAME
|
|
11
|
+
from constants import DOCKER_COMPOSE_FILE_NAME
|
|
12
|
+
from exceptions import DockerComposeError
|
|
13
|
+
from utils.docker_compose import run_docker_compose_command
|
|
14
|
+
from utils.services import find_matching_service
|
|
15
|
+
|
|
16
|
+
LINE_LENGTH = 40
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
20
|
+
parser = subparsers.add_parser("status", help="View status of a service")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"service_name",
|
|
23
|
+
help="Name of the service to view status for",
|
|
24
|
+
nargs="?",
|
|
25
|
+
default=None,
|
|
26
|
+
)
|
|
27
|
+
parser.set_defaults(func=status)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_status_output(status_json: str) -> str:
|
|
31
|
+
# Docker compose ps is line delimited json, so this constructs this into an array we can use
|
|
32
|
+
service_statuses = status_json.split("\n")[:-1]
|
|
33
|
+
output = []
|
|
34
|
+
output.append("-" * LINE_LENGTH)
|
|
35
|
+
for service_status in service_statuses:
|
|
36
|
+
service = json.loads(service_status)
|
|
37
|
+
name = service["Service"]
|
|
38
|
+
state = service["State"]
|
|
39
|
+
health = service.get("Health", "N/A")
|
|
40
|
+
ports = service.get("Publishers", [])
|
|
41
|
+
running_for = service.get("RunningFor", "N/A")
|
|
42
|
+
|
|
43
|
+
output.append(f"{name}")
|
|
44
|
+
output.append(f"Status: {state}")
|
|
45
|
+
output.append(f"Health: {health}")
|
|
46
|
+
output.append(f"Uptime: {running_for}")
|
|
47
|
+
|
|
48
|
+
if ports:
|
|
49
|
+
output.append("Ports:")
|
|
50
|
+
for port in ports:
|
|
51
|
+
output.append(
|
|
52
|
+
f" {port['PublishedPort']} -> {port['TargetPort']}/{port['Protocol']}"
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
output.append("No ports exposed")
|
|
56
|
+
|
|
57
|
+
output.append("") # Empty line for readability
|
|
58
|
+
|
|
59
|
+
return "\n".join(output)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def status(args: Namespace) -> None:
|
|
63
|
+
"""Start a service and its dependencies."""
|
|
64
|
+
service_name = args.service_name
|
|
65
|
+
try:
|
|
66
|
+
service = find_matching_service(service_name)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(e)
|
|
69
|
+
exit(1)
|
|
70
|
+
modes = service.config.modes
|
|
71
|
+
# TODO: allow custom modes to be used
|
|
72
|
+
mode_to_view = "default"
|
|
73
|
+
mode_dependencies = modes[mode_to_view]
|
|
74
|
+
service_config_file_path = os.path.join(
|
|
75
|
+
service.repo_path, DEVSERVICES_DIR_NAME, DOCKER_COMPOSE_FILE_NAME
|
|
76
|
+
)
|
|
77
|
+
mode_dependencies = " ".join(modes[mode_to_view])
|
|
78
|
+
try:
|
|
79
|
+
status_json = run_docker_compose_command(
|
|
80
|
+
f"-f {service_config_file_path} ps {mode_dependencies} --format json"
|
|
81
|
+
).stdout
|
|
82
|
+
except DockerComposeError as dce:
|
|
83
|
+
print(f"Failed to get status for {service.name}: {dce.stderr}")
|
|
84
|
+
exit(1)
|
|
85
|
+
# If the service is not running, the status_json will be empty
|
|
86
|
+
if not status_json:
|
|
87
|
+
print(f"{service.name} is not running")
|
|
88
|
+
return
|
|
89
|
+
output = f"Service: {service.name}\n\n"
|
|
90
|
+
output += format_status_output(status_json)
|
|
91
|
+
output += "=" * LINE_LENGTH
|
|
92
|
+
sys.stdout.write(output + "\n")
|
|
93
|
+
sys.stdout.flush()
|
commands/stop.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from argparse import _SubParsersAction
|
|
5
|
+
from argparse import ArgumentParser
|
|
6
|
+
from argparse import Namespace
|
|
7
|
+
|
|
8
|
+
from constants import DEVSERVICES_DIR_NAME
|
|
9
|
+
from constants import DOCKER_COMPOSE_FILE_NAME
|
|
10
|
+
from exceptions import DockerComposeError
|
|
11
|
+
from utils.console import Status
|
|
12
|
+
from utils.docker_compose import run_docker_compose_command
|
|
13
|
+
from utils.services import find_matching_service
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
|
|
17
|
+
parser = subparsers.add_parser("stop", help="Stop a service and its dependencies")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"service_name", help="Name of the service to stop", nargs="?", default=None
|
|
20
|
+
)
|
|
21
|
+
parser.set_defaults(func=stop)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def stop(args: Namespace) -> None:
|
|
25
|
+
"""Stop a service and its dependencies."""
|
|
26
|
+
service_name = args.service_name
|
|
27
|
+
try:
|
|
28
|
+
service = find_matching_service(service_name)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(e)
|
|
31
|
+
exit(1)
|
|
32
|
+
modes = service.config.modes
|
|
33
|
+
# TODO: allow custom modes to be used
|
|
34
|
+
mode_to_stop = "default"
|
|
35
|
+
mode_dependencies = " ".join(modes[mode_to_stop])
|
|
36
|
+
service_config_file_path = os.path.join(
|
|
37
|
+
service.repo_path, DEVSERVICES_DIR_NAME, DOCKER_COMPOSE_FILE_NAME
|
|
38
|
+
)
|
|
39
|
+
with Status(f"Stopping {service.name}", f"{service.name} stopped") as status:
|
|
40
|
+
try:
|
|
41
|
+
run_docker_compose_command(
|
|
42
|
+
f"-f {service_config_file_path} down {mode_dependencies}"
|
|
43
|
+
)
|
|
44
|
+
except DockerComposeError as dce:
|
|
45
|
+
status.print(f"Failed to stop {service.name}: {dce.stderr}")
|
|
46
|
+
exit(1)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from constants import DEVSERVICES_DIR_NAME
|
|
8
|
+
from constants import DOCKER_COMPOSE_FILE_NAME
|
|
9
|
+
from exceptions import ConfigNotFoundError
|
|
10
|
+
from exceptions import ConfigParseError
|
|
11
|
+
from exceptions import ConfigValidationError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
VALID_VERSIONS = [0.1]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Dependency:
|
|
19
|
+
description: str
|
|
20
|
+
link: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ServiceConfig:
|
|
25
|
+
version: float
|
|
26
|
+
service_name: str
|
|
27
|
+
dependencies: dict[str, Dependency]
|
|
28
|
+
modes: dict[str, list[str]]
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
self._validate()
|
|
32
|
+
|
|
33
|
+
def _validate(self) -> None:
|
|
34
|
+
if not self.version:
|
|
35
|
+
raise ConfigValidationError("Version is required in service config")
|
|
36
|
+
|
|
37
|
+
if self.version not in VALID_VERSIONS:
|
|
38
|
+
raise ConfigValidationError(
|
|
39
|
+
f"Invalid version '{self.version}' in service config"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not self.service_name:
|
|
43
|
+
raise ConfigValidationError("Service name is required in service config")
|
|
44
|
+
|
|
45
|
+
if "default" not in self.modes:
|
|
46
|
+
raise ConfigValidationError("Default mode is required in service config")
|
|
47
|
+
|
|
48
|
+
for mode, services in self.modes.items():
|
|
49
|
+
if not isinstance(services, list):
|
|
50
|
+
raise ConfigValidationError(f"Services in mode '{mode}' must be a list")
|
|
51
|
+
for service in services:
|
|
52
|
+
if service not in self.dependencies:
|
|
53
|
+
raise ConfigValidationError(
|
|
54
|
+
f"Service '{service}' in mode '{mode}' is not defined in dependencies"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_service_config_from_file(repo_path: str) -> ServiceConfig:
|
|
59
|
+
config_path = os.path.join(
|
|
60
|
+
repo_path, DEVSERVICES_DIR_NAME, DOCKER_COMPOSE_FILE_NAME
|
|
61
|
+
)
|
|
62
|
+
if not os.path.exists(config_path):
|
|
63
|
+
raise ConfigNotFoundError(f"Config file not found in directory: {config_path}")
|
|
64
|
+
with open(config_path, "r") as stream:
|
|
65
|
+
try:
|
|
66
|
+
config = yaml.safe_load(stream)
|
|
67
|
+
except yaml.YAMLError as yml_error:
|
|
68
|
+
raise ConfigParseError(
|
|
69
|
+
f"Error parsing config file: {yml_error}"
|
|
70
|
+
) from yml_error
|
|
71
|
+
|
|
72
|
+
if "x-sentry-service-config" not in config:
|
|
73
|
+
raise ConfigParseError(
|
|
74
|
+
"Config file does not contain 'x-sentry-service-config' key"
|
|
75
|
+
)
|
|
76
|
+
service_config_data = config.get("x-sentry-service-config")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
dependencies = {
|
|
80
|
+
key: Dependency(**value)
|
|
81
|
+
for key, value in service_config_data.get("dependencies", {}).items()
|
|
82
|
+
}
|
|
83
|
+
except TypeError as type_error:
|
|
84
|
+
raise ConfigParseError(
|
|
85
|
+
f"Error parsing service dependencies: {type_error}"
|
|
86
|
+
) from type_error
|
|
87
|
+
|
|
88
|
+
service_config = ServiceConfig(
|
|
89
|
+
version=service_config_data.get("version"),
|
|
90
|
+
service_name=service_config_data.get("service_name"),
|
|
91
|
+
dependencies=dependencies,
|
|
92
|
+
modes=service_config_data.get("modes", {}),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return service_config
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: devservices
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Requires-Python: >=3.12
|
|
5
|
+
Requires-Dist: pyyaml
|
|
6
|
+
Requires-Dist: sentry-devenv
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: black; extra == "dev"
|
|
9
|
+
Requires-Dist: mypy; extra == "dev"
|
|
10
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
13
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
commands/list_dependencies.py,sha256=Yi3VO0YGgVnuvZHuQgZt5lqifEKf_GpJx_nTjAu4mV4,1172
|
|
3
|
+
commands/list_services.py,sha256=KBE2lIYZiHhJDBRp8NT33xzby0-hM1dKzO3sP4UcMtk,872
|
|
4
|
+
commands/logs.py,sha256=_zwiendvSVXvvjfM29nApQYdn1kZPc2xLdWwfr_sKgE,1577
|
|
5
|
+
commands/start.py,sha256=zDqnaa9XHR0uOO3nUheuA-lZOiKAOFdhORlNojwZRFk,1632
|
|
6
|
+
commands/status.py,sha256=VFY-M3Je93m29XVMPf8pXlR5ZAA1KA_ywcU5HjgcKpA,3068
|
|
7
|
+
commands/stop.py,sha256=mdR3hTUpdm4__AcLCam8aRNlt0JJrMcTatlUqBJ2aBs,1622
|
|
8
|
+
configs/service_config.py,sha256=jyeNjQmkreL7mGQPP3y_aAiDUS4uyzCfHYrRhW23F7c,3108
|
|
9
|
+
utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
utils/console.py,sha256=hweyYeX6RB9tXJ9AabZfxuXMDZ9duXgz_zrYLJhLeeQ,1945
|
|
11
|
+
utils/devenv.py,sha256=qxh_vwM6gTMAjOtAJPBU0iWPkjkUXIrNsoGs4yt_9-k,786
|
|
12
|
+
utils/docker_compose.py,sha256=P2XFBjIL1avMmHdyHnX5FE3YwQD-eKtIbvlEhYR6Dz4,534
|
|
13
|
+
utils/services.py,sha256=--FTnonJ9C8U1HRNwZOkxZyXpAWwDV4guLB_p_8ISzw,1871
|
|
14
|
+
devservices-0.0.1.dist-info/METADATA,sha256=DHUHhE2RdTPqR6LJx29oSjj3-8n97TExoGpoCzx9zuA,348
|
|
15
|
+
devservices-0.0.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
16
|
+
devservices-0.0.1.dist-info/entry_points.txt,sha256=471CQlH2h8oMig0LMF-TqmYZ8UCFVzPxJgn4gxYIGws,42
|
|
17
|
+
devservices-0.0.1.dist-info/top_level.txt,sha256=wq_0FL0YzIpZhRP7mwJCU1oveQLmA4Ub4vQ6z5D_CKg,23
|
|
18
|
+
devservices-0.0.1.dist-info/RECORD,,
|
utils/__init__.py
ADDED
|
File without changes
|
utils/console.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ANIMATION_FRAMES = ("⠟", "⠯", "⠷", "⠾", "⠽", "⠻")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Status:
|
|
13
|
+
"""Shows loading status in the terminal."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, start_message: str | None = None, end_message: str | None = None
|
|
17
|
+
) -> None:
|
|
18
|
+
self.start_message = start_message
|
|
19
|
+
self.end_message = end_message
|
|
20
|
+
self._stop_loading = threading.Event()
|
|
21
|
+
self._loading_thread = threading.Thread(target=self._loading_animation)
|
|
22
|
+
self._exception_occured = False
|
|
23
|
+
|
|
24
|
+
def print(self, message: str) -> None:
|
|
25
|
+
sys.stdout.write("\r" + message + "\n")
|
|
26
|
+
sys.stdout.flush()
|
|
27
|
+
|
|
28
|
+
def start(self) -> None:
|
|
29
|
+
if self.start_message:
|
|
30
|
+
print(self.start_message)
|
|
31
|
+
self._loading_thread.start()
|
|
32
|
+
|
|
33
|
+
def stop(self) -> None:
|
|
34
|
+
self._stop_loading.set()
|
|
35
|
+
self._loading_thread.join()
|
|
36
|
+
sys.stdout.write("\r")
|
|
37
|
+
sys.stdout.flush()
|
|
38
|
+
if self.end_message and not self._exception_occured:
|
|
39
|
+
print(self.end_message)
|
|
40
|
+
|
|
41
|
+
def _loading_animation(self) -> None:
|
|
42
|
+
idx = 0
|
|
43
|
+
while not self._stop_loading.is_set():
|
|
44
|
+
sys.stdout.write("\r" + ANIMATION_FRAMES[idx % len(ANIMATION_FRAMES)] + " ")
|
|
45
|
+
sys.stdout.flush()
|
|
46
|
+
idx += 1
|
|
47
|
+
time.sleep(0.1)
|
|
48
|
+
|
|
49
|
+
def __enter__(self) -> Status:
|
|
50
|
+
self.start()
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def __exit__(
|
|
54
|
+
self,
|
|
55
|
+
exc_type: type[BaseException] | None,
|
|
56
|
+
exc_inst: BaseException | None,
|
|
57
|
+
exc_tb: TracebackType | None,
|
|
58
|
+
) -> bool:
|
|
59
|
+
self._exception_occured = exc_type is not None
|
|
60
|
+
self.stop()
|
|
61
|
+
if exc_type:
|
|
62
|
+
if exc_type in (KeyboardInterrupt,):
|
|
63
|
+
# Don't print anything if the user interrupts the process
|
|
64
|
+
return True
|
|
65
|
+
else:
|
|
66
|
+
return False
|
|
67
|
+
return False
|
utils/devenv.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from configparser import ConfigParser
|
|
5
|
+
from configparser import NoOptionError
|
|
6
|
+
from configparser import NoSectionError
|
|
7
|
+
|
|
8
|
+
from devenv.constants import home
|
|
9
|
+
from devenv.lib.config import read_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_coderoot() -> str:
|
|
13
|
+
config_path = os.path.join(home, ".config", "sentry-devenv", "config.ini")
|
|
14
|
+
try:
|
|
15
|
+
devenv_config: ConfigParser = read_config(config_path)
|
|
16
|
+
return devenv_config.get("devenv", "coderoot", fallback="")
|
|
17
|
+
except (FileNotFoundError, NoSectionError, NoOptionError):
|
|
18
|
+
# TODO: Handle the case where there is no config file or the coderoot is not set
|
|
19
|
+
raise Exception("Failed to read code root from config")
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise Exception(f"Failed to read config: {e}")
|
utils/docker_compose.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from exceptions import DockerComposeError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_docker_compose_command(command: str) -> subprocess.CompletedProcess[str]:
|
|
9
|
+
cmd = ["docker", "compose"] + command.split()
|
|
10
|
+
try:
|
|
11
|
+
return subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
12
|
+
except subprocess.CalledProcessError as e:
|
|
13
|
+
raise DockerComposeError(
|
|
14
|
+
command=command,
|
|
15
|
+
returncode=e.returncode,
|
|
16
|
+
stdout=e.stdout,
|
|
17
|
+
stderr=e.stderr,
|
|
18
|
+
)
|
utils/services.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from configs.service_config import ServiceConfig
|
|
7
|
+
from exceptions import ConfigNotFoundError
|
|
8
|
+
from exceptions import ConfigParseError
|
|
9
|
+
from exceptions import ConfigValidationError
|
|
10
|
+
from exceptions import ServiceNotFoundError
|
|
11
|
+
from utils.devenv import get_coderoot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Service:
|
|
16
|
+
name: str
|
|
17
|
+
repo_path: str
|
|
18
|
+
config: ServiceConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_local_services(coderoot: str) -> list[Service]:
|
|
22
|
+
"""Get a list of services in the coderoot."""
|
|
23
|
+
from configs.service_config import load_service_config_from_file
|
|
24
|
+
|
|
25
|
+
services = []
|
|
26
|
+
for repo in os.listdir(coderoot):
|
|
27
|
+
repo_path = os.path.join(coderoot, repo)
|
|
28
|
+
try:
|
|
29
|
+
service_config = load_service_config_from_file(repo_path)
|
|
30
|
+
except (ConfigNotFoundError, ConfigParseError, ConfigValidationError):
|
|
31
|
+
continue
|
|
32
|
+
service_name = service_config.service_name
|
|
33
|
+
services.append(
|
|
34
|
+
Service(
|
|
35
|
+
name=service_name,
|
|
36
|
+
repo_path=repo_path,
|
|
37
|
+
config=service_config,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
return services
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def find_matching_service(service_name: str | None = None) -> Service:
|
|
44
|
+
"""Find a service with the given name."""
|
|
45
|
+
if service_name is None:
|
|
46
|
+
from configs.service_config import load_service_config_from_file
|
|
47
|
+
|
|
48
|
+
repo_path = os.getcwd()
|
|
49
|
+
service_config = load_service_config_from_file(repo_path)
|
|
50
|
+
|
|
51
|
+
return Service(
|
|
52
|
+
name=service_config.service_name,
|
|
53
|
+
repo_path=repo_path,
|
|
54
|
+
config=service_config,
|
|
55
|
+
)
|
|
56
|
+
coderoot = get_coderoot()
|
|
57
|
+
services = get_local_services(coderoot)
|
|
58
|
+
for service in services:
|
|
59
|
+
if service.name.lower() == service_name.lower():
|
|
60
|
+
return service
|
|
61
|
+
raise ServiceNotFoundError(f'Service "{service_name}" not found')
|