ptctools 0.1.1__py3-none-any.whl → 0.2.0__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.
ptctools/cli.py CHANGED
@@ -3,11 +3,8 @@
3
3
  import click
4
4
 
5
5
  from ptctools import __version__
6
- from ptctools import stack
7
- from ptctools import volume
8
- from ptctools import db
6
+ from ptctools import docker
9
7
  from ptctools import utils
10
- from ptctools import config
11
8
 
12
9
 
13
10
  @click.group()
@@ -17,11 +14,11 @@ def main():
17
14
  pass
18
15
 
19
16
 
20
- main.add_command(stack.cli, name="stack")
21
- main.add_command(volume.cli, name="volume")
22
- main.add_command(db.cli, name="db")
17
+ # Docker commands (via Portainer Docker proxy)
18
+ main.add_command(docker.cli, name="docker")
19
+
20
+ # Local utility commands (no Portainer needed)
23
21
  main.add_command(utils.cli, name="utils")
24
- main.add_command(config.cli, name="config")
25
22
 
26
23
 
27
24
  if __name__ == "__main__":
ptctools/config.py CHANGED
@@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
  class ConfigError(PortainerError):
21
21
  """Exception for Docker config operations."""
22
+
22
23
  pass
23
24
 
24
25
 
@@ -29,7 +30,13 @@ def cli():
29
30
 
30
31
 
31
32
  @cli.command("set")
32
- @click.option("--url", "-u", required=True, help="Portainer base URL")
33
+ @click.option(
34
+ "--url",
35
+ "-u",
36
+ envvar="PORTAINER_URL",
37
+ required=True,
38
+ help="Portainer base URL (or PORTAINER_URL env var)",
39
+ )
33
40
  @click.option("--name", "-n", required=True, help="Config name")
34
41
  @click.option("--data", "-d", default=None, help="Config data (string content)")
35
42
  @click.option(
@@ -41,9 +48,7 @@ def cli():
41
48
  help="Read config data from file",
42
49
  )
43
50
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
44
- @click.option(
45
- "--force", is_flag=True, help="Replace existing config if it exists"
46
- )
51
+ @click.option("--force", is_flag=True, help="Replace existing config if it exists")
47
52
  def set_config(
48
53
  url: str,
49
54
  name: str,
@@ -57,13 +62,13 @@ def set_config(
57
62
  Examples:
58
63
 
59
64
  # Create from inline data
60
- ptctools config set -u https://portainer.example.com -n my-config -d "config content"
65
+ ptctools docker config set -u https://portainer.example.com -n my-config -d "config content"
61
66
 
62
67
  # Create from file
63
- ptctools config set -u https://portainer.example.com -n nginx.conf -f ./nginx.conf
68
+ ptctools docker config set -u https://portainer.example.com -n nginx.conf -f ./nginx.conf
64
69
 
65
70
  # Replace existing config
66
- ptctools config set -u https://portainer.example.com -n my-config -d "new content" --force
71
+ ptctools docker config set -u https://portainer.example.com -n my-config -d "new content" --force
67
72
  """
68
73
  # Validate that exactly one of --data or --file is provided
69
74
  if data is None and file_path is None:
@@ -88,7 +93,7 @@ def set_config(
88
93
 
89
94
  try:
90
95
  client = get_portainer_docker_client(portainer_url, access_token, endpoint_id)
91
-
96
+
92
97
  # Check if config already exists
93
98
  existing = None
94
99
  for config in client.configs.list():
@@ -119,7 +124,13 @@ def set_config(
119
124
 
120
125
 
121
126
  @cli.command("get")
122
- @click.option("--url", "-u", required=True, help="Portainer base URL")
127
+ @click.option(
128
+ "--url",
129
+ "-u",
130
+ envvar="PORTAINER_URL",
131
+ required=True,
132
+ help="Portainer base URL (or PORTAINER_URL env var)",
133
+ )
123
134
  @click.option("--name", "-n", required=True, help="Config name")
124
135
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
125
136
  def get_config_cmd(url: str, name: str, endpoint_id: int):
@@ -138,7 +149,7 @@ def get_config_cmd(url: str, name: str, endpoint_id: int):
138
149
 
139
150
  try:
140
151
  client = get_portainer_docker_client(portainer_url, access_token, endpoint_id)
141
-
152
+
142
153
  config = None
143
154
  for c in client.configs.list():
144
155
  if c.name == name:
@@ -158,7 +169,13 @@ def get_config_cmd(url: str, name: str, endpoint_id: int):
158
169
 
159
170
 
160
171
  @cli.command("list")
161
- @click.option("--url", "-u", required=True, help="Portainer base URL")
172
+ @click.option(
173
+ "--url",
174
+ "-u",
175
+ envvar="PORTAINER_URL",
176
+ required=True,
177
+ help="Portainer base URL (or PORTAINER_URL env var)",
178
+ )
162
179
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
163
180
  def list_configs_cmd(url: str, endpoint_id: int):
164
181
  """List all Docker Swarm configs."""
@@ -195,7 +212,13 @@ def list_configs_cmd(url: str, endpoint_id: int):
195
212
 
196
213
 
197
214
  @cli.command("delete")
198
- @click.option("--url", "-u", required=True, help="Portainer base URL")
215
+ @click.option(
216
+ "--url",
217
+ "-u",
218
+ envvar="PORTAINER_URL",
219
+ required=True,
220
+ help="Portainer base URL (or PORTAINER_URL env var)",
221
+ )
199
222
  @click.option("--name", "-n", required=True, help="Config name")
200
223
  @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
201
224
  def delete_config_cmd(url: str, name: str, endpoint_id: int):
@@ -211,7 +234,7 @@ def delete_config_cmd(url: str, name: str, endpoint_id: int):
211
234
 
212
235
  try:
213
236
  client = get_portainer_docker_client(portainer_url, access_token, endpoint_id)
214
-
237
+
215
238
  config = None
216
239
  for c in client.configs.list():
217
240
  if c.name == name:
ptctools/db.py CHANGED
@@ -10,7 +10,11 @@ import traceback
10
10
  import click
11
11
  import docker
12
12
 
13
- from ptctools._portainer import get_portainer_docker_client, run_container, PortainerError
13
+ from ptctools._portainer import (
14
+ get_portainer_docker_client,
15
+ run_container,
16
+ PortainerError,
17
+ )
14
18
  from ptctools._s3 import parse_s3_uri, is_s3_uri, get_s3_endpoint, get_s3_credentials
15
19
 
16
20
  logger = logging.getLogger(__name__)
@@ -18,6 +22,7 @@ logger = logging.getLogger(__name__)
18
22
 
19
23
  class DatabaseError(PortainerError):
20
24
  """Exception for database operations."""
25
+
21
26
  pass
22
27
 
23
28
 
@@ -42,7 +47,7 @@ def run_mc_command(
42
47
  """Run minio/mc command in ephemeral container.
43
48
 
44
49
  The volume is mounted at /data in the container.
45
-
50
+
46
51
  Raises:
47
52
  DatabaseError: If Docker operation fails
48
53
  """
@@ -54,7 +59,7 @@ def run_mc_command(
54
59
 
55
60
  try:
56
61
  client = get_portainer_docker_client(portainer_url, access_token, endpoint_id)
57
-
62
+
58
63
  return run_container(
59
64
  client=client,
60
65
  image="minio/mc:latest",
@@ -82,7 +87,7 @@ def backup_database(
82
87
  s3_secret_key: str | None,
83
88
  ) -> None:
84
89
  """Backup database using pg_dump via exec, then optionally upload to S3 with mc.
85
-
90
+
86
91
  Raises:
87
92
  DatabaseError: If backup fails
88
93
  """
@@ -101,13 +106,15 @@ def backup_database(
101
106
  # Step 1: Run pg_dump inside the database container
102
107
  click.echo(" Running pg_dump...")
103
108
  cmd = f"pg_dump -U {db_user} {db_name} | gzip > {backup_file} && stat -c %s {backup_file}"
104
-
109
+
105
110
  try:
106
111
  exit_code, output_stream = container.exec_run(
107
112
  ["sh", "-c", cmd],
108
113
  demux=False,
109
114
  )
110
- output_text = output_stream.decode("utf-8", errors="replace") if output_stream else ""
115
+ output_text = (
116
+ output_stream.decode("utf-8", errors="replace") if output_stream else ""
117
+ )
111
118
  except docker.errors.APIError as e:
112
119
  click.echo(f" ✗ pg_dump failed: {e}")
113
120
  raise DatabaseError(f"pg_dump failed: {e}") from e
@@ -142,7 +149,9 @@ def backup_database(
142
149
 
143
150
  if not s3_access_key or not s3_secret_key:
144
151
  click.echo(" ✗ S3 credentials required (S3_ACCESS_KEY, S3_SECRET_KEY)")
145
- raise DatabaseError("S3 credentials required (S3_ACCESS_KEY, S3_SECRET_KEY)")
152
+ raise DatabaseError(
153
+ "S3 credentials required (S3_ACCESS_KEY, S3_SECRET_KEY)"
154
+ )
146
155
 
147
156
  click.echo(f" Uploading to S3: s3://{bucket}/{s3_path}...")
148
157
 
@@ -179,14 +188,18 @@ def backup_database(
179
188
  ["sh", "-c", f"base64 {backup_file}"],
180
189
  demux=False,
181
190
  )
182
- b64_content = b64_output.decode("utf-8", errors="replace") if b64_output else ""
191
+ b64_content = (
192
+ b64_output.decode("utf-8", errors="replace") if b64_output else ""
193
+ )
183
194
  except docker.errors.APIError as e:
184
195
  click.echo(" ✗ Failed to read backup file from container")
185
196
  raise DatabaseError("Failed to read backup file from container") from e
186
197
 
187
198
  if read_exit_code != 0:
188
199
  click.echo(f" ✗ Failed to read backup file: exit code {read_exit_code}")
189
- raise DatabaseError(f"Failed to read backup file: exit code {read_exit_code}")
200
+ raise DatabaseError(
201
+ f"Failed to read backup file: exit code {read_exit_code}"
202
+ )
190
203
 
191
204
  # Decode and save to output file
192
205
  import base64
@@ -227,7 +240,7 @@ def restore_database(
227
240
  s3_secret_key: str | None,
228
241
  ) -> None:
229
242
  """Restore database from local file or S3.
230
-
243
+
231
244
  Raises:
232
245
  DatabaseError: If restore fails
233
246
  """
@@ -260,7 +273,9 @@ def restore_database(
260
273
 
261
274
  if not s3_access_key or not s3_secret_key:
262
275
  click.echo(" ✗ S3 credentials required (S3_ACCESS_KEY, S3_SECRET_KEY)")
263
- raise DatabaseError("S3 credentials required (S3_ACCESS_KEY, S3_SECRET_KEY)")
276
+ raise DatabaseError(
277
+ "S3 credentials required (S3_ACCESS_KEY, S3_SECRET_KEY)"
278
+ )
264
279
 
265
280
  click.echo(f" Downloading from S3: s3://{bucket}/{s3_path}...")
266
281
 
@@ -322,7 +337,9 @@ def restore_database(
322
337
 
323
338
  if write_exit_code != 0:
324
339
  click.echo(f" ✗ Failed to write backup file: exit code {write_exit_code}")
325
- raise DatabaseError(f"Failed to write backup file: exit code {write_exit_code}")
340
+ raise DatabaseError(
341
+ f"Failed to write backup file: exit code {write_exit_code}"
342
+ )
326
343
 
327
344
  # Append remaining chunks
328
345
  for chunk in chunks[1:]:
@@ -350,7 +367,9 @@ def restore_database(
350
367
  ["sh", "-c", psql_cmd],
351
368
  demux=False,
352
369
  )
353
- output = output_stream.decode("utf-8", errors="replace") if output_stream else ""
370
+ output = (
371
+ output_stream.decode("utf-8", errors="replace") if output_stream else ""
372
+ )
354
373
  except docker.errors.APIError as e:
355
374
  click.echo(f" ✗ Restore failed: {e}")
356
375
  raise DatabaseError(f"Restore failed: {e}") from e
@@ -381,7 +400,13 @@ def cli():
381
400
 
382
401
 
383
402
  @cli.command()
384
- @click.option("--url", "-u", required=True, help="Portainer base URL")
403
+ @click.option(
404
+ "--url",
405
+ "-u",
406
+ envvar="PORTAINER_URL",
407
+ required=True,
408
+ help="Portainer base URL (or PORTAINER_URL env var)",
409
+ )
385
410
  @click.option("--container-id", "-c", required=True, help="Database container ID")
386
411
  @click.option(
387
412
  "--volume-name", "-v", required=True, help="Volume name where database stores data"
@@ -417,9 +442,9 @@ def backup(
417
442
  """Backup PostgreSQL database to local file or S3.
418
443
 
419
444
  Examples:
420
- ptctools db backup -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -o /tmp/backup.sql.gz
445
+ ptctools docker db backup -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -o /tmp/backup.sql.gz
421
446
 
422
- ptctools db backup -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -o s3://mybucket/backups/backup.sql.gz --s3-endpoint https://s3.<region>.amazonaws.com
447
+ ptctools docker db backup -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -o s3://mybucket/backups/backup.sql.gz --s3-endpoint https://s3.<region>.amazonaws.com
423
448
  """
424
449
  access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
425
450
  if not access_token:
@@ -477,7 +502,13 @@ def backup(
477
502
 
478
503
 
479
504
  @cli.command()
480
- @click.option("--url", "-u", required=True, help="Portainer base URL")
505
+ @click.option(
506
+ "--url",
507
+ "-u",
508
+ envvar="PORTAINER_URL",
509
+ required=True,
510
+ help="Portainer base URL (or PORTAINER_URL env var)",
511
+ )
481
512
  @click.option("--container-id", "-c", required=True, help="Database container ID")
482
513
  @click.option(
483
514
  "--volume-name", "-v", required=True, help="Volume name where database stores data"
@@ -514,9 +545,9 @@ def restore(
514
545
  """Restore PostgreSQL database from local file or S3.
515
546
 
516
547
  Examples:
517
- ptctools db restore -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -i /tmp/backup.sql.gz
548
+ ptctools docker db restore -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -i /tmp/backup.sql.gz
518
549
 
519
- ptctools db restore -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -i s3://mybucket/backups/backup.sql.gz --s3-endpoint https://s3.<region>.amazonaws.com
550
+ ptctools docker db restore -u https://portainer.example.com -c abc123 -v db_data --db-user postgres --db-name mydb -i s3://mybucket/backups/backup.sql.gz --s3-endpoint https://s3.<region>.amazonaws.com
520
551
  """
521
552
  access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
522
553
  if not access_token:
ptctools/docker.py ADDED
@@ -0,0 +1,29 @@
1
+ """Docker management commands (via Portainer Docker proxy)."""
2
+
3
+ import click
4
+
5
+ from ptctools import stack
6
+ from ptctools import secret
7
+ from ptctools import config
8
+ from ptctools import volume
9
+ from ptctools import db
10
+
11
+
12
+ @click.group()
13
+ def cli():
14
+ """Docker management commands.
15
+
16
+ These commands work with Docker environments via Portainer.
17
+ Some commands (stack, secret, config) require Docker Swarm.
18
+ """
19
+ pass
20
+
21
+
22
+ # Swarm-only commands
23
+ cli.add_command(stack.cli, name="stack")
24
+ cli.add_command(secret.cli, name="secret")
25
+ cli.add_command(config.cli, name="config")
26
+
27
+ # Any Docker commands
28
+ cli.add_command(volume.cli, name="volume")
29
+ cli.add_command(db.cli, name="db")
ptctools/secret.py ADDED
@@ -0,0 +1,146 @@
1
+ """Secret management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import click
11
+ import docker.errors
12
+
13
+ from ptctools._portainer import (
14
+ get_portainer_docker_client,
15
+ PortainerError,
16
+ )
17
+
18
+
19
+ class SecretError(PortainerError):
20
+ """Exception for secret operations."""
21
+
22
+ pass
23
+
24
+
25
+ @click.group()
26
+ def cli():
27
+ """Secret management commands."""
28
+ pass
29
+
30
+
31
+ @cli.command()
32
+ @click.option(
33
+ "--url",
34
+ "-u",
35
+ envvar="PORTAINER_URL",
36
+ required=True,
37
+ help="Portainer base URL (or PORTAINER_URL env var)",
38
+ )
39
+ @click.option("--endpoint-id", "-e", type=int, default=1, help="Portainer endpoint ID")
40
+ @click.option(
41
+ "--file",
42
+ "-f",
43
+ "file_path",
44
+ type=click.Path(exists=True, path_type=Path),
45
+ help="Read secret from file",
46
+ )
47
+ @click.option(
48
+ "--value", "-v", help="Secret value (use stdin or --file for sensitive data)"
49
+ )
50
+ @click.argument("name")
51
+ def create(
52
+ url: str,
53
+ endpoint_id: int,
54
+ file_path: Path | None,
55
+ value: str | None,
56
+ name: str,
57
+ ):
58
+ """Create a Docker Swarm secret.
59
+
60
+ NAME is the name of the secret to create.
61
+
62
+ The secret value can be provided via:
63
+ - stdin: echo "secret" | ptctools docker secret create -u URL my_secret
64
+ - file: ptctools docker secret create -u URL -f /path/to/file my_secret
65
+ - value: ptctools docker secret create -u URL -v "secret" my_secret
66
+
67
+ Example:
68
+ echo "postgresql://user:pass@db:5432/mydb" | ptctools docker secret create -u https://portainer.example.com litellm_db_dsn
69
+ """
70
+ access_token = os.environ.get("PORTAINER_ACCESS_TOKEN")
71
+ if not access_token:
72
+ click.echo(
73
+ "Error: Missing PORTAINER_ACCESS_TOKEN environment variable", err=True
74
+ )
75
+ sys.exit(1)
76
+
77
+ # Determine secret data source
78
+ secret_data: bytes | None = None
79
+
80
+ if file_path:
81
+ # Read from file
82
+ secret_data = file_path.read_bytes()
83
+ elif value:
84
+ # Use provided value
85
+ secret_data = value.encode("utf-8")
86
+ elif not sys.stdin.isatty():
87
+ # Read from stdin
88
+ secret_data = sys.stdin.read().encode("utf-8")
89
+ else:
90
+ click.echo(
91
+ "Error: Secret value required. Use --file, --value, or pipe via stdin.",
92
+ err=True,
93
+ )
94
+ sys.exit(1)
95
+
96
+ # Strip trailing newline if present (common when piping from echo)
97
+ if secret_data.endswith(b"\n"):
98
+ secret_data = secret_data.rstrip(b"\n")
99
+
100
+ portainer_url = url.rstrip("/")
101
+
102
+ click.echo(f"Portainer URL: {portainer_url}")
103
+ click.echo(f"Endpoint ID: {endpoint_id}")
104
+ click.echo(f"Secret Name: {name}")
105
+ click.echo()
106
+
107
+ try:
108
+ docker_client = get_portainer_docker_client(
109
+ portainer_url, access_token, endpoint_id
110
+ )
111
+
112
+ # Check if secret already exists
113
+ try:
114
+ existing = docker_client.secrets.get(name)
115
+ click.echo(
116
+ f"Error: Secret '{name}' already exists (ID: {existing.id[:12]})",
117
+ err=True,
118
+ )
119
+ click.echo(
120
+ "Use 'docker secret rm' to remove it first, or choose a different name.",
121
+ err=True,
122
+ )
123
+ sys.exit(1)
124
+ except docker.errors.NotFound:
125
+ pass # Good, secret doesn't exist
126
+
127
+ # Create the secret
128
+ click.echo(f"Creating secret '{name}'...")
129
+ secret = docker_client.secrets.create(name=name, data=secret_data)
130
+ click.echo(f"Secret created successfully!")
131
+ click.echo(f" ID: {secret.id}")
132
+ click.echo(f" Name: {name}")
133
+ click.echo()
134
+ click.echo("Done!")
135
+ sys.exit(0)
136
+
137
+ except docker.errors.APIError as e:
138
+ click.echo(f"Error: {e.explanation or str(e)}", err=True)
139
+ click.echo()
140
+ click.echo("Failed!")
141
+ sys.exit(1)
142
+ except SecretError as e:
143
+ click.echo(f"Error: {e}", err=True)
144
+ click.echo()
145
+ click.echo("Failed!")
146
+ sys.exit(1)
ptctools/stack.py CHANGED
@@ -19,12 +19,24 @@ from ptctools._portainer import (
19
19
  )
20
20
  from ptctools.portainer_client.openapi_client.api.stacks_api import StacksApi
21
21
  from ptctools.portainer_client.openapi_client.api.users_api import UsersApi
22
- from ptctools.portainer_client.openapi_client.api.resource_controls_api import ResourceControlsApi
23
- from ptctools.portainer_client.openapi_client.models.stacks_swarm_stack_from_file_content_payload import StacksSwarmStackFromFileContentPayload
24
- from ptctools.portainer_client.openapi_client.models.stacks_update_swarm_stack_payload import StacksUpdateSwarmStackPayload
25
- from ptctools.portainer_client.openapi_client.models.resourcecontrols_resource_control_create_payload import ResourcecontrolsResourceControlCreatePayload
26
- from ptctools.portainer_client.openapi_client.models.resourcecontrols_resource_control_update_payload import ResourcecontrolsResourceControlUpdatePayload
27
- from ptctools.portainer_client.openapi_client.models.portainer_resource_control_type import PortainerResourceControlType
22
+ from ptctools.portainer_client.openapi_client.api.resource_controls_api import (
23
+ ResourceControlsApi,
24
+ )
25
+ from ptctools.portainer_client.openapi_client.models.stacks_swarm_stack_from_file_content_payload import (
26
+ StacksSwarmStackFromFileContentPayload,
27
+ )
28
+ from ptctools.portainer_client.openapi_client.models.stacks_update_swarm_stack_payload import (
29
+ StacksUpdateSwarmStackPayload,
30
+ )
31
+ from ptctools.portainer_client.openapi_client.models.resourcecontrols_resource_control_create_payload import (
32
+ ResourcecontrolsResourceControlCreatePayload,
33
+ )
34
+ from ptctools.portainer_client.openapi_client.models.resourcecontrols_resource_control_update_payload import (
35
+ ResourcecontrolsResourceControlUpdatePayload,
36
+ )
37
+ from ptctools.portainer_client.openapi_client.models.portainer_resource_control_type import (
38
+ PortainerResourceControlType,
39
+ )
28
40
  from ptctools.portainer_client.openapi_client.exceptions import ApiException
29
41
 
30
42
  logger = logging.getLogger(__name__)
@@ -32,6 +44,7 @@ logger = logging.getLogger(__name__)
32
44
 
33
45
  class StackError(PortainerError):
34
46
  """Exception for stack operations."""
47
+
35
48
  pass
36
49
 
37
50
 
@@ -63,7 +76,13 @@ def cli():
63
76
 
64
77
 
65
78
  @cli.command()
66
- @click.option("--url", "-u", required=True, help="Portainer base URL")
79
+ @click.option(
80
+ "--url",
81
+ "-u",
82
+ envvar="PORTAINER_URL",
83
+ required=True,
84
+ help="Portainer base URL (or PORTAINER_URL env var)",
85
+ )
67
86
  @click.option("--stack-name", "-n", required=True, help="Stack name")
68
87
  @click.option(
69
88
  "--stack-file",
@@ -113,11 +132,15 @@ def deploy(
113
132
  user_teams = []
114
133
  for m in memberships:
115
134
  if m.team_id:
116
- user_teams.append({"Id": m.team_id, "Name": f"Team {m.team_id}"})
117
-
135
+ user_teams.append(
136
+ {"Id": m.team_id, "Name": f"Team {m.team_id}"}
137
+ )
138
+
118
139
  if user_teams:
119
140
  team_id = user_teams[0]["Id"]
120
- click.echo(f"Auto-detected team: {user_teams[0]['Name']} (ID: {team_id})")
141
+ click.echo(
142
+ f"Auto-detected team: {user_teams[0]['Name']} (ID: {team_id})"
143
+ )
121
144
  else:
122
145
  click.echo(
123
146
  "Error: ownership=team but no team found and --team-id not specified",
@@ -160,7 +183,9 @@ def deploy(
160
183
 
161
184
  if existing_stack_id is not None:
162
185
  # Update existing stack
163
- click.echo(f"Updating existing stack: {stack_name} (ID: {existing_stack_id})...")
186
+ click.echo(
187
+ f"Updating existing stack: {stack_name} (ID: {existing_stack_id})..."
188
+ )
164
189
  payload = StacksUpdateSwarmStackPayload(
165
190
  stack_file_content=stack_content,
166
191
  env=env_vars,
@@ -168,14 +193,18 @@ def deploy(
168
193
  pull_image=True,
169
194
  )
170
195
  response = stacks_api.stack_update(
171
- id=existing_stack_id, endpoint_id=endpoint_id, body=payload.to_dict()
196
+ id=existing_stack_id,
197
+ endpoint_id=endpoint_id,
198
+ body=payload.to_dict(),
172
199
  )
173
200
  click.echo("Stack updated successfully!")
174
201
  click.echo(json.dumps(response.to_dict(), indent=2))
175
202
  else:
176
203
  # Create new stack - need swarm ID first
177
204
  click.echo("Getting swarm ID...")
178
- docker_client = get_portainer_docker_client(portainer_url, access_token, endpoint_id)
205
+ docker_client = get_portainer_docker_client(
206
+ portainer_url, access_token, endpoint_id
207
+ )
179
208
  swarm_info = docker_client.swarm.attrs
180
209
  swarm_id = swarm_info.get("ID") if swarm_info else None
181
210
  if not swarm_id:
@@ -198,7 +227,7 @@ def deploy(
198
227
  # Apply access control
199
228
  if ownership:
200
229
  click.echo()
201
-
230
+
202
231
  # Get final stack ID
203
232
  final_stack_id = None
204
233
  stacks = stacks_api.stack_list()
@@ -221,7 +250,9 @@ def deploy(
221
250
  teams=[],
222
251
  users=[user_id],
223
252
  )
224
- rc_api.resource_control_update(id=rc.id, body=rc_payload.to_dict())
253
+ rc_api.resource_control_update(
254
+ id=rc.id, body=rc_payload.to_dict()
255
+ )
225
256
  click.echo("✓ Access control set to private")
226
257
 
227
258
  elif ownership == "team" and team_id:
@@ -232,7 +263,9 @@ def deploy(
232
263
  teams=[team_id],
233
264
  users=[],
234
265
  )
235
- rc_api.resource_control_update(id=rc.id, body=rc_payload.to_dict())
266
+ rc_api.resource_control_update(
267
+ id=rc.id, body=rc_payload.to_dict()
268
+ )
236
269
  click.echo(f"✓ Access control set to team {team_id}")
237
270
  else:
238
271
  rc_payload = ResourcecontrolsResourceControlCreatePayload(
@@ -253,7 +286,9 @@ def deploy(
253
286
  teams=[],
254
287
  users=[],
255
288
  )
256
- rc_api.resource_control_update(id=rc.id, body=rc_payload.to_dict())
289
+ rc_api.resource_control_update(
290
+ id=rc.id, body=rc_payload.to_dict()
291
+ )
257
292
  click.echo("✓ Access control set to public")
258
293
  else:
259
294
  rc_payload = ResourcecontrolsResourceControlCreatePayload(
@@ -272,7 +307,9 @@ def deploy(
272
307
 
273
308
  except ApiException as e:
274
309
  error_msg = str(e.body) if e.body else str(e.reason)
275
- logger.error("Stack operation failed: %s\n%s", error_msg, traceback.format_exc())
310
+ logger.error(
311
+ "Stack operation failed: %s\n%s", error_msg, traceback.format_exc()
312
+ )
276
313
  click.echo(f"Error: {error_msg}", err=True)
277
314
  click.echo()
278
315
  click.echo("Failed!")