poetry-plugin-ivcap 0.1.0__tar.gz

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.
@@ -0,0 +1,5 @@
1
+ # Initial version authors
2
+
3
+ * Max Ott <max.ott@csiro.au>
4
+
5
+ # Partial list of contributors
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Commonwealth Scientific and Industrial Research Organisation (CSIRO) ABN 41 687 119 230
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.3
2
+ Name: poetry-plugin-ivcap
3
+ Version: 0.1.0
4
+ Summary: A custom Poetry command for IVCAP deployments
5
+ License: MIT
6
+ Author: Max Ott
7
+ Author-email: max.ott@csiro.au
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: humanize (>=4.12.3,<5.0.0)
17
+ Requires-Dist: pydantic (>=2.11.5,<3.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # poetry-plugin-ivcap
21
+
22
+ A custom Poetry plugin that adds a `poetry ivcap` command for local and Docker-based deployments.
23
+
24
+ ## Example Configuration
25
+
26
+ Add to your `pyproject.toml`:
27
+
28
+ ```toml
29
+ [tool.poetry-plugin-ivcap]
30
+ default_target = "docker"
31
+ docker_tag = "myapp:dev"
32
+ ```
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ poetry self add poetry-plugin-ivcap
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ poetry ivcap
44
+ poetry ivcap docker
45
+ ```
46
+
47
+ ## Development
48
+
49
+ ### Build the Plugin Package
50
+
51
+ ```bash
52
+ poetry build
53
+ ```
54
+
55
+ This creates .whl and .tar.gz files in the dist/ directory.
56
+
57
+ ### Publish to PyPI
58
+
59
+ Create an account at https://pypi.org/account/register/
60
+
61
+ Add your credentials:
62
+ ```bash
63
+ poetry config pypi-token.pypi <your-token>
64
+ ```
65
+
66
+ Publish:
67
+ ```bash
68
+ poetry publish --build
69
+ ```
70
+
71
+ ### Optional: Test on TestPyPI First
72
+
73
+ To verify your setup without publishing to the real PyPI:
74
+
75
+ ```bash
76
+ poetry config repositories.test-pypi https://test.pypi.org/legacy/
77
+ poetry publish -r test-pypi --build
78
+ ```
79
+
80
+ Then test installing via:
81
+
82
+ ```bash
83
+ poetry self add --source test-pypi poetry-plugin-deploy
84
+ ```
85
+
@@ -0,0 +1,65 @@
1
+ # poetry-plugin-ivcap
2
+
3
+ A custom Poetry plugin that adds a `poetry ivcap` command for local and Docker-based deployments.
4
+
5
+ ## Example Configuration
6
+
7
+ Add to your `pyproject.toml`:
8
+
9
+ ```toml
10
+ [tool.poetry-plugin-ivcap]
11
+ default_target = "docker"
12
+ docker_tag = "myapp:dev"
13
+ ```
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ poetry self add poetry-plugin-ivcap
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ poetry ivcap
25
+ poetry ivcap docker
26
+ ```
27
+
28
+ ## Development
29
+
30
+ ### Build the Plugin Package
31
+
32
+ ```bash
33
+ poetry build
34
+ ```
35
+
36
+ This creates .whl and .tar.gz files in the dist/ directory.
37
+
38
+ ### Publish to PyPI
39
+
40
+ Create an account at https://pypi.org/account/register/
41
+
42
+ Add your credentials:
43
+ ```bash
44
+ poetry config pypi-token.pypi <your-token>
45
+ ```
46
+
47
+ Publish:
48
+ ```bash
49
+ poetry publish --build
50
+ ```
51
+
52
+ ### Optional: Test on TestPyPI First
53
+
54
+ To verify your setup without publishing to the real PyPI:
55
+
56
+ ```bash
57
+ poetry config repositories.test-pypi https://test.pypi.org/legacy/
58
+ poetry publish -r test-pypi --build
59
+ ```
60
+
61
+ Then test installing via:
62
+
63
+ ```bash
64
+ poetry self add --source test-pypi poetry-plugin-deploy
65
+ ```
@@ -0,0 +1,185 @@
1
+ #
2
+ # Copyright (c) 2025 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ import os
7
+ import re
8
+ import sys
9
+ import tempfile
10
+ from typing import Dict, Optional
11
+ from pydantic import BaseModel, Field
12
+ import subprocess
13
+
14
+ from .util import command_exists, get_name
15
+
16
+ DOCKER_BUILD_TEMPLATE = """
17
+ docker buildx build
18
+ -t #DOCKER_NAME#
19
+ --platform linux/#ARCH#
20
+ --build-arg VERSION=#VERSION#
21
+ --build-arg BUILD_PLATFORM=linux/#ARCH#
22
+ -f #PROJECT_DIR#/#DOCKERFILE#
23
+ --load #PROJECT_DIR#
24
+ """
25
+
26
+ DOCKER_RUN_TEMPLATE = """
27
+ docker run -it
28
+ -p #PORT#:#PORT#
29
+ --platform=linux/#ARCH#
30
+ --rm \
31
+ #NAME#_#ARCH#:#TAG# --port #PORT#
32
+ """
33
+
34
+ class DockerConfig(BaseModel):
35
+ name: Optional[str] = Field(None)
36
+ tag: Optional[str] = Field(None)
37
+ arch: Optional[str] = Field(None)
38
+ version: Optional[str] = Field(None)
39
+ dockerfile: Optional[str] = Field("Dockerfile")
40
+ project_dir: Optional[str] = Field(".")
41
+
42
+ @property
43
+ def docker_name(self) -> str:
44
+ return f"{self.name}_{self.arch}:{self.tag}"
45
+
46
+ def from_build_template(self, data: dict) -> str:
47
+ pdata = data.get("tool", {}).get("poetry-plugin-ivcap", {})
48
+ template = pdata.get("docker-build-template", DOCKER_BUILD_TEMPLATE).strip()
49
+ t = template.strip()\
50
+ .replace("#DOCKER_NAME#", self.docker_name)\
51
+ .replace("#NAME#", self.name)\
52
+ .replace("#TAG#", self.tag)\
53
+ .replace("#ARCH#", self.arch)\
54
+ .replace("#VERSION#", self.version)\
55
+ .replace("#DOCKERFILE#", self.dockerfile)\
56
+ .replace("#PROJECT_DIR#", self.project_dir)\
57
+ .split()
58
+ return t
59
+
60
+ def from_run_template(self, data) -> str:
61
+ pdata = data.get("tool", {}).get("poetry-plugin-ivcap", {})
62
+ template = pdata.get("docker-run-template", DOCKER_RUN_TEMPLATE).strip()
63
+ opts = pdata.get("docker-run-opts", {"port": 8080})
64
+ t = template.strip()\
65
+ .replace("#NAME#", self.name)\
66
+ .replace("#TAG#", self.tag)\
67
+ .replace("#ARCH#", self.arch)\
68
+ .replace("#VERSION#", self.version)\
69
+ .replace("#PROJECT_DIR#", self.project_dir)
70
+
71
+ for key, value in opts.items():
72
+ t = t.replace(f"#{key.upper()}#", str(value))
73
+
74
+ return t.split()
75
+
76
+
77
+ def docker_build(data: dict, line, arch = None) -> None:
78
+ check_docker_cmd(line)
79
+ config = _docker_cfg(data, line, arch)
80
+ build_cmd = config.from_build_template(data)
81
+ line(f"<info>INFO: {' '.join(build_cmd)}</info>")
82
+ process = subprocess.Popen(build_cmd, stdout=sys.stdout, stderr=sys.stderr)
83
+ exit_code = process.wait()
84
+ if exit_code != 0:
85
+ line(f"<error>ERROR: Docker build failed with exit code {exit_code}</error>")
86
+ else:
87
+ line("<info>INFO: Docker build completed successfully</info>")
88
+ return config.docker_name
89
+
90
+ def docker_run(data: dict, line) -> None:
91
+ check_docker_cmd(line)
92
+ config = _docker_cfg(data, line)
93
+ build_run = config.from_run_template(data)
94
+ line(f"<info>INFO: {' '.join(build_run)}</info>")
95
+ process = subprocess.Popen(build_run, stdout=sys.stdout, stderr=sys.stderr)
96
+ exit_code = process.wait()
97
+ if exit_code != 0:
98
+ line(f"<error>ERROR: Docker run failed with exit code {exit_code}</error>")
99
+ else:
100
+ line("<info>INFO: Docker run completed successfully</info>")
101
+
102
+ def _docker_cfg(data: dict, line, arch = None) -> DockerConfig:
103
+ project_data = data.get("project", {})
104
+ name = get_name(data)
105
+ version = project_data.get("version", None)
106
+
107
+ pdata = data.get("tool", {}).get("poetry-plugin-ivcap", {})
108
+ config = DockerConfig(name=name, version=version, **pdata.get("docker", {}))
109
+ if arch:
110
+ # override architecture if provided
111
+ config.arch = arch
112
+
113
+ if not config.version:
114
+ try:
115
+ config.version = subprocess.check_output(['git', 'describe', '--tags', '--abbrev=0']).decode().strip()
116
+ except Exception as e:
117
+ line(f"<error>Error retrieving latest tag: {e}</error>")
118
+ config.version = "???"
119
+
120
+ if not config.tag:
121
+ try:
122
+ config.tag = subprocess.check_output(['git', 'rev-parse', "--short", 'HEAD']).decode().strip()
123
+ except Exception as e:
124
+ line(f"<warning>WARN: retrieving commit hash: {e}</warning>")
125
+ config.tag = "latest"
126
+
127
+ if not config.arch:
128
+ try:
129
+ config.arch = subprocess.check_output(['uname', '-m']).decode().strip()
130
+ except Exception as e:
131
+ line(f"<error>ERROR: cannot obtain build architecture: {e}</error>")
132
+ os.exit(1)
133
+
134
+ return config
135
+
136
+ def docker_push(docker_img, line):
137
+ push_cmd = ["ivcap", "package", "push", "--force", "--local", docker_img]
138
+ line(f"<debug>Running: {' '.join(push_cmd)} </debug>")
139
+ p1 = subprocess.Popen(push_cmd, stdout=subprocess.PIPE, stderr=sys.stderr)
140
+
141
+ with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp:
142
+ tmp_path = tmp.name
143
+
144
+ try:
145
+ # Pipe its output to tee (or to the screen via /dev/tty)
146
+ p2 = subprocess.Popen(
147
+ ["tee", tmp_path],
148
+ stdin=p1.stdout,
149
+ stdout=sys.stdout,
150
+ stderr=sys.stderr
151
+ )
152
+ p1.stdout.close()
153
+ # Wait for both to finish
154
+ p2.communicate()
155
+ exit_code = p1.wait()
156
+ if exit_code != 0:
157
+ line(f"<error>ERROR: package push failed with exit code {exit_code}</error>")
158
+ sys.exit(1)
159
+
160
+ # Lookginf for "45a06508-5c3a-4678-8e6d-e6399bf27538/gene_onology_term_mapper_amd64:9a9a7cc pushed\n"
161
+ pattern = re.compile(
162
+ r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/[^\s]+) pushed'
163
+ )
164
+ package_name = None
165
+ with open(tmp_path, 'r') as f:
166
+ for l in f:
167
+ match = pattern.search(l)
168
+ if match:
169
+ package_name = match.group(1)
170
+ break
171
+
172
+ if not package_name:
173
+ line("<error>ERROR: No package name found in output</error>")
174
+ sys.exit(1)
175
+
176
+ line("<info>INFO: package push completed successfully</info>")
177
+ return package_name
178
+ finally:
179
+ if os.path.exists(tmp_path):
180
+ os.remove(tmp_path)
181
+
182
+ def check_docker_cmd(line):
183
+ if not command_exists("docker"):
184
+ line("<error>'docker' command not found. Please install it first.</error>")
185
+ sys.exit(1)
@@ -0,0 +1,147 @@
1
+ #
2
+ # Copyright (c) 2025 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import uuid
12
+ import humanize
13
+
14
+ from .docker import _docker_cfg, docker_build, docker_push
15
+ from .util import command_exists, execute_subprocess_and_capture_output, get_name, string_to_number
16
+
17
+ def docker_publish(data, line):
18
+ check_ivcap_cmd(line)
19
+ #dname = docker_build(data, line, arch="amd64")
20
+ dname = "gene_onology_term_mapper_amd64:9a9a7cc"
21
+ print(dname)
22
+
23
+ size_cmd = ["docker", "inspect", "--format='{{.Size}}'", dname]
24
+ line(f"<debug>Running: {' '.join(size_cmd)} </debug>")
25
+ size = string_to_number(subprocess.check_output(size_cmd).decode())
26
+ if size is None:
27
+ line("<error>Failed to retrieve image size</error>")
28
+ return
29
+ line(f"<info>INFO: Image size {humanize.naturalsize(size)}</info>")
30
+ pkg_name = docker_push(dname, line)
31
+
32
+ def service_register(data, line):
33
+ check_ivcap_cmd(line)
34
+ config = data.get("tool", {}).get("poetry-plugin-ivcap", {})
35
+
36
+ service = config.get("service-file")
37
+ if not service:
38
+ line("<error>Missing 'service-file' in [tool.poetry-plugin-ivcap]</error>")
39
+ return
40
+
41
+ dcfg = _docker_cfg(data, line, "amd64")
42
+ pkg_cmd = ["ivcap", "package", "list", dcfg.docker_name]
43
+ line(f"<debug>Running: {' '.join(pkg_cmd)} </debug>")
44
+ pkg = subprocess.check_output(pkg_cmd).decode()
45
+ if not pkg or pkg == "":
46
+ line(f"<error>No package '{dcfg.docker_name}' found. Please build and publish it first.</error>")
47
+ return
48
+ service_id = get_service_id(data, line)
49
+
50
+ cmd = ["poetry", "run", "python", service, "--print-service-description"]
51
+ line(f"<debug>Running: {' '.join(cmd)} </debug>")
52
+ svc = subprocess.check_output(cmd).decode()
53
+
54
+ svc = svc.replace("#DOCKER_IMG#", pkg.strip())\
55
+ .replace("#SERVICE_ID#", service_id)
56
+
57
+ with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp:
58
+ tmp.write(svc)
59
+ tmp_path = tmp.name # Save the file name for subprocess
60
+
61
+ try:
62
+ policy = get_policy(data, line)
63
+ up_cmd = ["ivcap", "aspect", "update", "--policy", policy, service_id, "-f", tmp_path]
64
+ try:
65
+ line(f"<debug>Running: {' '.join(up_cmd)} </debug>")
66
+ jaid = subprocess.check_output(up_cmd).decode().strip()
67
+ p = re.compile(r'.*(urn:[^"]*)')
68
+ aid = p.search(jaid).group(1)
69
+ line(f"<info>INFO: service definition successfully uploaded - {aid}</info>")
70
+ except Exception as e:
71
+ line(f"<error>ERROR: cannot upload service definitiion: {e}</error>")
72
+ sys.exit(1)
73
+ finally:
74
+ if os.path.exists(tmp_path):
75
+ os.remove(tmp_path)
76
+
77
+ def tool_register(data, line):
78
+ check_ivcap_cmd(line)
79
+ config = data.get("tool", {}).get("poetry-plugin-ivcap", {})
80
+
81
+ service = config.get("service-file")
82
+ if not service:
83
+ line("<error>Missing 'service-file' in [tool.poetry-plugin-ivcap]</error>")
84
+ return
85
+
86
+ cmd = ["poetry", "run", "python", service, "--print-tool-description"]
87
+ line(f"<debug>Running: {' '.join(cmd)} </debug>")
88
+ svc = subprocess.check_output(cmd).decode()
89
+
90
+ service_id = get_service_id(data, line)
91
+ svc = svc.replace("#SERVICE_ID#", service_id)
92
+
93
+ with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp:
94
+ tmp.write(svc)
95
+ tmp_path = tmp.name # Save the file name for subprocess
96
+
97
+ try:
98
+ policy = get_policy(data, line)
99
+ up_cmd = ["ivcap", "aspect", "update", "--policy", policy, service_id, "-f", tmp_path]
100
+ try:
101
+ line(f"<debug>Running: {' '.join(up_cmd)} </debug>")
102
+ jaid = subprocess.check_output(up_cmd).decode().strip()
103
+ p = re.compile(r'.*(urn:[^"]*)')
104
+ aid = p.search(jaid).group(1)
105
+ line(f"<info>INFO: tool description successfully uploaded - {aid}</info>")
106
+ except Exception as e:
107
+ line(f"<error>ERROR: cannot upload tool description: {e}</error>")
108
+ sys.exit(1)
109
+ finally:
110
+ if os.path.exists(tmp_path):
111
+ os.remove(tmp_path)
112
+
113
+ def get_service_id(data, line):
114
+ service_id = data.get("tool", {}).get("poetry-plugin-ivcap", {}).get("service-id")
115
+ if not service_id:
116
+ service_id = create_service_id(data, line)
117
+ return service_id
118
+
119
+ def create_service_id(data, line):
120
+ check_ivcap_cmd(line)
121
+ name = get_name(data)
122
+ account_id = get_account_id(data, line)
123
+ id = uuid.uuid5(uuid.NAMESPACE_DNS, f"{name}{account_id}")
124
+ return f"urn:ivcap:service:{id}"
125
+
126
+ def get_policy(data, line):
127
+ policy = data.get("tool", {}).get("poetry-plugin-ivcap", {}).get("policy")
128
+ if not policy:
129
+ policy = "urn:ivcap:policy:ivcap.open.metadata"
130
+ return policy
131
+
132
+ def get_account_id(data, line):
133
+ check_ivcap_cmd(line)
134
+ cmd = ["ivcap", "context", "get", "account-id"]
135
+ line(f"<debug>Running: {' '.join(cmd)} </debug>")
136
+ try:
137
+ account_id = subprocess.check_output(cmd).decode().strip()
138
+ return account_id
139
+ except subprocess.CalledProcessError as e:
140
+ line(f"<error>Error retrieving account ID: {e}</error>")
141
+ sys.exit(1)
142
+
143
+ def check_ivcap_cmd(line):
144
+ if not command_exists("ivcap"):
145
+ line("<error>'ivcap' command not found. Please install the IVCAP CLI tool.</error>")
146
+ line("<error>... see https://github.com/ivcap-works/ivcap-cli?tab=readme-ov-file#install-released-binaries for instructions</error>")
147
+ os.exit(1)
@@ -0,0 +1,70 @@
1
+ #
2
+ # Copyright (c) 2025 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ from poetry.plugins.application_plugin import ApplicationPlugin
7
+ from cleo.commands.command import Command
8
+ from cleo.helpers import argument
9
+ import subprocess
10
+
11
+ from .ivcap import create_service_id, service_register, tool_register
12
+ from .docker import docker_build, docker_run
13
+ from .ivcap import docker_publish
14
+
15
+ class IvcapCommand(Command):
16
+ name = "ivcap"
17
+ description = "IVCAP plugin `poetry ivcap <subcommand>`"
18
+ help = """\
19
+ IVCAP plugin
20
+
21
+ Available subcommands:
22
+ run Run the configured service
23
+ deploy Deploy using configured settings
24
+
25
+ Example:
26
+ poetry ivcap run
27
+ """
28
+ arguments = [
29
+ argument("subcommand", optional=True, description="Subcommand: run, deploy, etc.")
30
+ ]
31
+
32
+ def handle(self):
33
+ poetry = self.application.poetry
34
+ data = poetry.pyproject.data
35
+
36
+ sub = self.argument("subcommand")
37
+ if sub == "run":
38
+ self.run_service(data)
39
+ elif sub == "docker-build":
40
+ docker_build(data, self.line)
41
+ elif sub == "docker-run":
42
+ docker_run(data, self.line)
43
+ elif sub == "docker-publish":
44
+ docker_publish(data, self.line)
45
+ elif sub == "service-register":
46
+ service_register(data, self.line)
47
+ elif sub == "create-service-id":
48
+ sid = create_service_id(data, self.line)
49
+ print(sid)
50
+ elif sub == "tool-register":
51
+ tool_register(data, self.line)
52
+ else:
53
+ self.line(f"<error>Unknown subcommand: {sub}</error>")
54
+
55
+ def run_service(self, data):
56
+ config = data.get("tool", {}).get("poetry-plugin-ivcap", {})
57
+
58
+ service = config.get("service-file")
59
+ port = config.get("port")
60
+
61
+ if not service or not port:
62
+ self.line("<error>Missing 'service-file' or 'port' in [tool.poetry-plugin-ivcap]</error>")
63
+ return
64
+
65
+ self.line(f"<info>Running: python {service} --port {port}</info>")
66
+ subprocess.run(["poetry", "run", "python", service, "--port", str(port)])
67
+
68
+ class IvcapPlugin(ApplicationPlugin):
69
+ def activate(self, application):
70
+ application.command_loader.register_factory("ivcap", lambda: IvcapCommand())
@@ -0,0 +1,87 @@
1
+ #
2
+ # Copyright (c) 2025 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
3
+ # Use of this source code is governed by a BSD-style license that can be
4
+ # found in the LICENSE file. See the AUTHORS file for names of contributors.
5
+ #
6
+ import subprocess
7
+ import platform
8
+
9
+ def get_name(data) -> str:
10
+ project_data = data.get("project", {})
11
+ name = project_data.get("name").replace("-", "_").replace(".", "_")
12
+ return name
13
+
14
+
15
+ def command_exists(cmd: str) -> bool:
16
+ system = platform.system()
17
+
18
+ check_cmd = {
19
+ "Windows": ["where", cmd],
20
+ "Linux": ["which", cmd],
21
+ "Darwin": ["which", cmd],
22
+ }.get(system)
23
+
24
+ if not check_cmd:
25
+ raise RuntimeError(f"Unsupported OS: {system}")
26
+
27
+ try:
28
+ subprocess.run(check_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
29
+ return True
30
+ except subprocess.CalledProcessError:
31
+ return False
32
+
33
+ def string_to_number(s):
34
+ """Converts a string to a number (int)."""
35
+ s = s.replace("'", "") # Remove all single quotes
36
+ try:
37
+ return int(s.strip())
38
+ except ValueError as e:
39
+ print(e)
40
+ return None # Or raise an exception, depending on your needs
41
+
42
+ import io
43
+ import sys
44
+
45
+ def execute_subprocess_and_capture_output(command):
46
+ """
47
+ Executes a subprocess, prints all outputs to the console in real-time, and returns a full copy of the output.
48
+
49
+ Args:
50
+ command (list): A list of strings representing the command to execute.
51
+
52
+ Returns:
53
+ str: A string containing the full output of the subprocess.
54
+ """
55
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
56
+
57
+ full_output = ""
58
+
59
+ # Read stdout and stderr in real-time
60
+ while True:
61
+ stdout_line = process.stdout.readline()
62
+ stderr_line = process.stderr.readline()
63
+
64
+ if stdout_line:
65
+ print("Stdout:", stdout_line, end="") # Print to console immediately
66
+ sys.stdout.flush()
67
+ full_output += stdout_line
68
+ if stderr_line:
69
+ print("Stderr:", stderr_line, end="") # Print to console immediately
70
+ sys.stderr.flush()
71
+ full_output += stderr_line
72
+
73
+ # Check if the process has finished
74
+ if process.poll() is not None:
75
+ break
76
+
77
+ # Read any remaining output
78
+ for line in process.stdout:
79
+ print("Stdout:", line, end="")
80
+ sys.stdout.flush()
81
+ full_output += line
82
+ for line in process.stderr:
83
+ print("Stderr:", line, end="")
84
+ sys.stderr.flush()
85
+ full_output += line
86
+
87
+ return full_output
@@ -0,0 +1,20 @@
1
+ [tool.poetry]
2
+ name = "poetry-plugin-ivcap"
3
+ version = "0.1.0"
4
+ description = "A custom Poetry command for IVCAP deployments"
5
+ authors = ["Max Ott <max.ott@csiro.au>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ packages = [{ include = "poetry_plugin_ivcap" }]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.9"
12
+ pydantic = "^2.11.5"
13
+ humanize = "^4.12.3"
14
+
15
+ [tool.poetry.plugins."poetry.application.plugin"]
16
+ ivcap = "poetry_plugin_ivcap.plugin:IvcapPlugin"
17
+
18
+ [build-system]
19
+ requires = ["poetry-core"]
20
+ build-backend = "poetry.core.masonry.api"