plotly-cloud 0.3.0__tar.gz → 0.4.1__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.
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/PKG-INFO +1 -1
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/__init__.py +1 -1
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_api_types.py +27 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_commands.py +179 -5
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_deploy.py +167 -6
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/pyproject.toml +1 -1
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/.gitignore +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/LICENSE +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/README.md +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_changes.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_cloud_env.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_definitions.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_devtool_hooks.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_devtool_publish_rpc.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_oauth.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_parser.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/_run_sync.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/cli.py +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/cloud-env.toml +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/cloud_devtools.css +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/cloud_devtools.js +0 -0
- {plotly_cloud-0.3.0 → plotly_cloud-0.4.1}/plotly_cloud/exceptions.py +0 -0
|
@@ -56,3 +56,30 @@ class App(TypedDict):
|
|
|
56
56
|
class UserTeamMembership(TypedDict):
|
|
57
57
|
team: Team
|
|
58
58
|
role: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Build(TypedDict):
|
|
62
|
+
id: NotRequired[str]
|
|
63
|
+
revisionId: NotRequired[str]
|
|
64
|
+
imageRef: NotRequired[str]
|
|
65
|
+
sourceKey: NotRequired[str]
|
|
66
|
+
pythonVersion: NotRequired[str]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Revision(TypedDict):
|
|
70
|
+
id: str
|
|
71
|
+
workloadId: NotRequired[str]
|
|
72
|
+
revisionNumber: NotRequired[int]
|
|
73
|
+
status: NotRequired[str]
|
|
74
|
+
replicaSet: NotRequired[str]
|
|
75
|
+
createdBy: NotRequired[str]
|
|
76
|
+
createdByEmail: NotRequired[str]
|
|
77
|
+
createdByFirstName: NotRequired[str]
|
|
78
|
+
createdByLastName: NotRequired[str]
|
|
79
|
+
build: NotRequired[Build]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DashlogsNotFound(TypedDict):
|
|
83
|
+
error: str
|
|
84
|
+
reason: str
|
|
85
|
+
retention_days: int
|
|
@@ -34,6 +34,7 @@ from plotly_cloud._parser import ParsedArguments
|
|
|
34
34
|
|
|
35
35
|
from .exceptions import (
|
|
36
36
|
ApplicationError,
|
|
37
|
+
AuthenticationError,
|
|
37
38
|
CredentialError,
|
|
38
39
|
DashAppError,
|
|
39
40
|
ModuleImportError,
|
|
@@ -448,7 +449,7 @@ class PublishCommand(BaseCommand):
|
|
|
448
449
|
},
|
|
449
450
|
{
|
|
450
451
|
"name": "--team",
|
|
451
|
-
"help": "Team name
|
|
452
|
+
"help": "Team name to assign the app to (alternative to --team-id)",
|
|
452
453
|
},
|
|
453
454
|
{
|
|
454
455
|
"name": "--output",
|
|
@@ -641,7 +642,7 @@ class PublishCommand(BaseCommand):
|
|
|
641
642
|
team_name_or_slug = getattr(args, "team", None)
|
|
642
643
|
|
|
643
644
|
if team_name_or_slug and not team_id:
|
|
644
|
-
# Need to resolve team name
|
|
645
|
+
# Need to resolve team name to team_id
|
|
645
646
|
team_resolve_task = progress.add_task(f"Resolving team '{team_name_or_slug}'...", total=None)
|
|
646
647
|
try:
|
|
647
648
|
teams = await deploy_client.list_teams()
|
|
@@ -649,7 +650,6 @@ class PublishCommand(BaseCommand):
|
|
|
649
650
|
for team in teams:
|
|
650
651
|
if (
|
|
651
652
|
team.get("name", "").lower() == team_name_or_slug.lower()
|
|
652
|
-
or team.get("team_slug", "").lower() == team_name_or_slug.lower()
|
|
653
653
|
or team.get("id", "").lower() == team_name_or_slug.lower()
|
|
654
654
|
):
|
|
655
655
|
matching_team = team
|
|
@@ -787,6 +787,94 @@ class PublishCommand(BaseCommand):
|
|
|
787
787
|
console.print(f"Your app is available at: {format_app_url(app_url)}")
|
|
788
788
|
|
|
789
789
|
|
|
790
|
+
class ListCommand(BaseCommand):
|
|
791
|
+
"""List all apps for the authenticated user."""
|
|
792
|
+
|
|
793
|
+
name = "list"
|
|
794
|
+
group = "app"
|
|
795
|
+
short_description = "📋 List all your published apps"
|
|
796
|
+
description = "Display all applications published to Plotly Cloud."
|
|
797
|
+
arguments: List[CommandArgument] = [
|
|
798
|
+
{
|
|
799
|
+
"name": "--team",
|
|
800
|
+
"help": "Filter apps by team name",
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
"name": "--team-id",
|
|
804
|
+
"help": "Filter apps by team ID",
|
|
805
|
+
},
|
|
806
|
+
]
|
|
807
|
+
|
|
808
|
+
@classmethod
|
|
809
|
+
async def execute(cls, args: ParsedArguments) -> None:
|
|
810
|
+
"""Execute list command."""
|
|
811
|
+
client_id = cloud_config.get_oauth_client_id()
|
|
812
|
+
oauth_client = OAuthClient(client_id, token=getattr(args, "api_key", None))
|
|
813
|
+
|
|
814
|
+
if not await oauth_client.is_authenticated():
|
|
815
|
+
raise ApplicationError("Not authenticated. Please run 'plotly login' first.")
|
|
816
|
+
|
|
817
|
+
auth_token = await oauth_client.get_access_token()
|
|
818
|
+
if not auth_token:
|
|
819
|
+
raise ApplicationError("Unable to retrieve access token. Please try logging in again.")
|
|
820
|
+
|
|
821
|
+
async with DeploymentClient(oauth_client) as deploy_client:
|
|
822
|
+
# Resolve team name to team_id if needed
|
|
823
|
+
team_id = getattr(args, "team_id", None)
|
|
824
|
+
team_name_or_slug = getattr(args, "team", None)
|
|
825
|
+
|
|
826
|
+
if team_name_or_slug and not team_id:
|
|
827
|
+
teams = await deploy_client.list_teams()
|
|
828
|
+
for team in teams:
|
|
829
|
+
if (
|
|
830
|
+
team.get("name", "").lower() == team_name_or_slug.lower()
|
|
831
|
+
or team.get("id", "").lower() == team_name_or_slug.lower()
|
|
832
|
+
):
|
|
833
|
+
team_id = team.get("id")
|
|
834
|
+
break
|
|
835
|
+
if not team_id:
|
|
836
|
+
raise ApplicationError(
|
|
837
|
+
f"Team '{team_name_or_slug}' not found. Use 'plotly user teams' to list available teams."
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
apps = await deploy_client.list_apps(team_id)
|
|
841
|
+
|
|
842
|
+
if not apps:
|
|
843
|
+
console.print(Panel("No apps found.", title="📋 Apps", border_style="yellow"))
|
|
844
|
+
return
|
|
845
|
+
|
|
846
|
+
table = Table(show_header=True, header_style="bold blue", expand=True)
|
|
847
|
+
table.add_column("Name", style="cyan", no_wrap=True, ratio=3)
|
|
848
|
+
table.add_column("Status", style="white", no_wrap=True, ratio=1)
|
|
849
|
+
table.add_column("App ID", style="dim", no_wrap=True, ratio=1)
|
|
850
|
+
table.add_column("Published", style="magenta", no_wrap=True, ratio=1)
|
|
851
|
+
|
|
852
|
+
for app in apps:
|
|
853
|
+
status = app.get("status", "—")
|
|
854
|
+
status_info = REVISION_STATUS_MAP.get(status, {"label": status, "emoji": "?", "color": "white"})
|
|
855
|
+
status_display = (
|
|
856
|
+
f"{status_info['emoji']} [{status_info['color']}]{status_info['label']}[/{status_info['color']}]"
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
last_published = app.get("last_published", "—")
|
|
860
|
+
if last_published and last_published != "—":
|
|
861
|
+
last_published = last_published[:10]
|
|
862
|
+
|
|
863
|
+
app_id = app.get("id", "—")
|
|
864
|
+
app_id_short = app_id[:8] if app_id != "—" else "—"
|
|
865
|
+
|
|
866
|
+
table.add_row(
|
|
867
|
+
app.get("name", "—"),
|
|
868
|
+
status_display,
|
|
869
|
+
app_id_short,
|
|
870
|
+
last_published,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
console.print()
|
|
874
|
+
console.print(Panel.fit(table, title="📋 Your Apps", border_style="bold blue"))
|
|
875
|
+
console.print()
|
|
876
|
+
|
|
877
|
+
|
|
790
878
|
class StatusCommand(BaseCommand):
|
|
791
879
|
"""Get the status of an app published to Plotly Cloud."""
|
|
792
880
|
|
|
@@ -909,7 +997,6 @@ class TeamsCommand(BaseCommand):
|
|
|
909
997
|
# Create a table for the teams
|
|
910
998
|
table = Table(show_header=True, header_style="bold blue")
|
|
911
999
|
table.add_column("Name", style="cyan", no_wrap=True)
|
|
912
|
-
table.add_column("Slug", style="green")
|
|
913
1000
|
table.add_column("Team ID", style="white", overflow="fold")
|
|
914
1001
|
table.add_column("Created", style="magenta")
|
|
915
1002
|
|
|
@@ -917,7 +1004,6 @@ class TeamsCommand(BaseCommand):
|
|
|
917
1004
|
for team in teams:
|
|
918
1005
|
table.add_row(
|
|
919
1006
|
team.get("name", "—"),
|
|
920
|
-
team.get("team_slug", "—"),
|
|
921
1007
|
team.get("id", "—"),
|
|
922
1008
|
team.get("created_at", "—")[:10] if team.get("created_at") else "—", # Show only date
|
|
923
1009
|
)
|
|
@@ -932,6 +1018,94 @@ class TeamsCommand(BaseCommand):
|
|
|
932
1018
|
console.print()
|
|
933
1019
|
|
|
934
1020
|
|
|
1021
|
+
class LogsCommand(BaseCommand):
|
|
1022
|
+
"""Get build or runtime logs for a published app."""
|
|
1023
|
+
|
|
1024
|
+
name = "logs"
|
|
1025
|
+
group = "app"
|
|
1026
|
+
short_description = "📋 Get logs for a published app"
|
|
1027
|
+
description = "Retrieve build or runtime logs for your published application."
|
|
1028
|
+
arguments: List[CommandArgument] = [
|
|
1029
|
+
{
|
|
1030
|
+
"name": "--project-path",
|
|
1031
|
+
"default": ".",
|
|
1032
|
+
"help": "Path to project directory",
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
"name": "--config",
|
|
1036
|
+
"default": "plotly-cloud.toml",
|
|
1037
|
+
"help": "Path to configuration file",
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
"name": "--type",
|
|
1041
|
+
"default": "build",
|
|
1042
|
+
"choices": ["build", "runtime"],
|
|
1043
|
+
"help": "Type of logs to retrieve (default: build)",
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
"name": "--revision",
|
|
1047
|
+
"help": "Revision ID (default: latest revision)",
|
|
1048
|
+
},
|
|
1049
|
+
]
|
|
1050
|
+
|
|
1051
|
+
@classmethod
|
|
1052
|
+
async def execute(cls, args: ParsedArguments) -> None:
|
|
1053
|
+
"""Execute logs command."""
|
|
1054
|
+
project_path = os.path.abspath(args.project_path)
|
|
1055
|
+
config_path = get_config_path(project_path, args.config)
|
|
1056
|
+
config = load_deployment_config(config_path)
|
|
1057
|
+
|
|
1058
|
+
app_id = config.get("app_id")
|
|
1059
|
+
if not app_id:
|
|
1060
|
+
raise ApplicationError("No app_id found in configuration. Publish your app first using 'plotly publish'.")
|
|
1061
|
+
|
|
1062
|
+
client_id = cloud_config.get_oauth_client_id()
|
|
1063
|
+
oauth_client = OAuthClient(client_id, token=getattr(args, "api_key", None))
|
|
1064
|
+
|
|
1065
|
+
if not await oauth_client.is_authenticated():
|
|
1066
|
+
raise AuthenticationError("Not authenticated. Please run 'plotly login' first.")
|
|
1067
|
+
|
|
1068
|
+
auth_token = await oauth_client.get_access_token()
|
|
1069
|
+
if not auth_token:
|
|
1070
|
+
raise AuthenticationError("Unable to retrieve access token. Please try logging in again.")
|
|
1071
|
+
|
|
1072
|
+
log_type = getattr(args, "type", "build")
|
|
1073
|
+
revision_id = getattr(args, "revision", None)
|
|
1074
|
+
|
|
1075
|
+
async with DeploymentClient(oauth_client) as deploy_client:
|
|
1076
|
+
with Progress(
|
|
1077
|
+
SpinnerColumn(),
|
|
1078
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1079
|
+
console=console,
|
|
1080
|
+
) as progress:
|
|
1081
|
+
# Resolve revision ID if not provided
|
|
1082
|
+
if not revision_id:
|
|
1083
|
+
task = progress.add_task("Fetching latest revision...", total=None)
|
|
1084
|
+
revisions = await deploy_client.get_app_revisions(app_id)
|
|
1085
|
+
if not revisions:
|
|
1086
|
+
raise ApplicationError("No revisions found for this app.")
|
|
1087
|
+
revision_id = revisions[0]["id"]
|
|
1088
|
+
progress.update(task, description=f"✓ Using revision {revision_id[:8]}...")
|
|
1089
|
+
|
|
1090
|
+
# Fetch logs
|
|
1091
|
+
task = progress.add_task(f"Fetching {log_type} logs...", total=None)
|
|
1092
|
+
log_text, _ = await deploy_client.get_logs(app_id, revision_id, log_type)
|
|
1093
|
+
progress.remove_task(task)
|
|
1094
|
+
|
|
1095
|
+
if not log_text.strip():
|
|
1096
|
+
console.print(f"No {log_type} logs available.")
|
|
1097
|
+
return
|
|
1098
|
+
|
|
1099
|
+
console.print(
|
|
1100
|
+
Panel(
|
|
1101
|
+
log_text,
|
|
1102
|
+
title=f"📋 {log_type.title()} Logs",
|
|
1103
|
+
border_style="blue",
|
|
1104
|
+
expand=True,
|
|
1105
|
+
)
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
|
|
935
1109
|
class WhoamiCommand(BaseCommand):
|
|
936
1110
|
"""Show current user information."""
|
|
937
1111
|
|
|
@@ -11,7 +11,7 @@ import httpx
|
|
|
11
11
|
import tomli
|
|
12
12
|
import tomli_w
|
|
13
13
|
|
|
14
|
-
from plotly_cloud._api_types import App, AppRequest, Team
|
|
14
|
+
from plotly_cloud._api_types import App, AppRequest, DashlogsNotFound, Revision, Team, UserTeamMembership
|
|
15
15
|
from plotly_cloud._cloud_env import cloud_config
|
|
16
16
|
from plotly_cloud._definitions import AppDeploymentConfig
|
|
17
17
|
from plotly_cloud._oauth import OAuthClient
|
|
@@ -112,9 +112,7 @@ def should_exclude_path(path: str, exclude_patterns: set[str]) -> bool:
|
|
|
112
112
|
return False
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
async def create_deployment_zip(
|
|
116
|
-
project_path: str, output_path: str, skip_size_check: bool = False
|
|
117
|
-
) -> int:
|
|
115
|
+
async def create_deployment_zip(project_path: str, output_path: str, skip_size_check: bool = False) -> int:
|
|
118
116
|
"""
|
|
119
117
|
Create a zip file for deployment, excluding files based on .gitignore.
|
|
120
118
|
|
|
@@ -512,8 +510,8 @@ class DeploymentClient:
|
|
|
512
510
|
response = await self._client.get(url)
|
|
513
511
|
|
|
514
512
|
if response.status_code == 200:
|
|
515
|
-
|
|
516
|
-
return
|
|
513
|
+
memberships: list[UserTeamMembership] = response.json()["teams"]
|
|
514
|
+
return [m["team"] for m in memberships]
|
|
517
515
|
|
|
518
516
|
token_refreshed = await self._refresh_token_if_needed(response)
|
|
519
517
|
if token_refreshed:
|
|
@@ -524,3 +522,166 @@ class DeploymentClient:
|
|
|
524
522
|
raise _handle_error_response(response, "list teams")
|
|
525
523
|
except httpx.RequestError as e:
|
|
526
524
|
raise NetworkError("Failed to list teams", str(e)) from e
|
|
525
|
+
|
|
526
|
+
async def list_apps(self, team_id: Optional[str] = None) -> list[App]:
|
|
527
|
+
"""List all apps for the authenticated user.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
team_id: Optional team ID to filter apps by
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
List of App objects
|
|
534
|
+
|
|
535
|
+
Raises:
|
|
536
|
+
DeploymentClientError: If client is not initialized
|
|
537
|
+
APIError: If API call fails
|
|
538
|
+
NetworkError: If network request fails
|
|
539
|
+
"""
|
|
540
|
+
if not self._client:
|
|
541
|
+
raise DeploymentClientError("Client not initialized. Use within async context manager.")
|
|
542
|
+
|
|
543
|
+
url = f"https://{self.api_base_url}/api/apps"
|
|
544
|
+
params = {}
|
|
545
|
+
if team_id:
|
|
546
|
+
params["teamId"] = team_id
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
retry_count = 0
|
|
550
|
+
response = None
|
|
551
|
+
while retry_count <= 1:
|
|
552
|
+
response = await self._client.get(url, params=params)
|
|
553
|
+
|
|
554
|
+
if response.status_code == 200:
|
|
555
|
+
apps: list[App] = response.json()
|
|
556
|
+
return apps
|
|
557
|
+
|
|
558
|
+
token_refreshed = await self._refresh_token_if_needed(response)
|
|
559
|
+
if token_refreshed:
|
|
560
|
+
retry_count += 1
|
|
561
|
+
else:
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
raise _handle_error_response(response, "list apps")
|
|
565
|
+
except httpx.RequestError as e:
|
|
566
|
+
raise NetworkError("Failed to list apps", str(e)) from e
|
|
567
|
+
|
|
568
|
+
async def get_app_revisions(self, app_id: str) -> list[Revision]:
|
|
569
|
+
"""Get revisions for an application.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
app_id: Application ID
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
List of Revision objects
|
|
576
|
+
|
|
577
|
+
Raises:
|
|
578
|
+
DeploymentClientError: If client is not initialized
|
|
579
|
+
APIError: If API call fails
|
|
580
|
+
NetworkError: If network request fails
|
|
581
|
+
"""
|
|
582
|
+
if not self._client:
|
|
583
|
+
raise DeploymentClientError("Client not initialized. Use within async context manager.")
|
|
584
|
+
|
|
585
|
+
url = f"https://{self.api_base_url}/api/app/{app_id}/revisions"
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
retry_count = 0
|
|
589
|
+
response = None
|
|
590
|
+
while retry_count <= 1:
|
|
591
|
+
response = await self._client.get(url)
|
|
592
|
+
|
|
593
|
+
if response.status_code == 200:
|
|
594
|
+
data = response.json()
|
|
595
|
+
revisions: list[Revision] = data.get("revisions", data) if isinstance(data, dict) else data
|
|
596
|
+
return revisions
|
|
597
|
+
|
|
598
|
+
token_refreshed = await self._refresh_token_if_needed(response)
|
|
599
|
+
if token_refreshed:
|
|
600
|
+
retry_count += 1
|
|
601
|
+
else:
|
|
602
|
+
break
|
|
603
|
+
|
|
604
|
+
raise _handle_error_response(response, "get app revisions")
|
|
605
|
+
except httpx.RequestError as e:
|
|
606
|
+
raise NetworkError("Failed to get app revisions", str(e)) from e
|
|
607
|
+
|
|
608
|
+
async def get_logs(
|
|
609
|
+
self,
|
|
610
|
+
app_id: str,
|
|
611
|
+
revision_id: str,
|
|
612
|
+
log_type: str = "build",
|
|
613
|
+
continue_token: Optional[str] = None,
|
|
614
|
+
) -> tuple[str, Optional[str]]:
|
|
615
|
+
"""Get build or runtime logs for an app revision.
|
|
616
|
+
|
|
617
|
+
Uses the dashlogs streaming endpoints which return log lines
|
|
618
|
+
followed by a JSON meta line with continuation token.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
app_id: Application ID
|
|
622
|
+
revision_id: Revision ID
|
|
623
|
+
log_type: "build" or "runtime"
|
|
624
|
+
continue_token: Continuation token from a previous response
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Tuple of (log_text, next_continue_token).
|
|
628
|
+
next_continue_token is None if there are no more logs.
|
|
629
|
+
|
|
630
|
+
Raises:
|
|
631
|
+
DeploymentClientError: If client is not initialized
|
|
632
|
+
APIError: If API call fails (including 202 when logs not available)
|
|
633
|
+
NetworkError: If network request fails
|
|
634
|
+
"""
|
|
635
|
+
if not self._client:
|
|
636
|
+
raise DeploymentClientError("Client not initialized. Use within async context manager.")
|
|
637
|
+
|
|
638
|
+
url = f"https://{self.api_base_url}/api/app/{app_id}/dashlogs/{revision_id}/{log_type}"
|
|
639
|
+
params = {}
|
|
640
|
+
if continue_token:
|
|
641
|
+
params["continue"] = continue_token
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
retry_count = 0
|
|
645
|
+
response = None
|
|
646
|
+
while retry_count <= 1:
|
|
647
|
+
response = await self._client.get(url, params=params)
|
|
648
|
+
|
|
649
|
+
if response.status_code == 200:
|
|
650
|
+
body = response.text
|
|
651
|
+
# The last line is a JSON meta line with continuation info
|
|
652
|
+
lines = body.rstrip("\n").split("\n")
|
|
653
|
+
next_token = None
|
|
654
|
+
log_lines = lines
|
|
655
|
+
if lines:
|
|
656
|
+
try:
|
|
657
|
+
meta = json.loads(lines[-1])
|
|
658
|
+
next_token = meta.get("next_token")
|
|
659
|
+
log_lines = lines[:-1]
|
|
660
|
+
except (json.JSONDecodeError, ValueError):
|
|
661
|
+
pass
|
|
662
|
+
return "\n".join(log_lines), next_token
|
|
663
|
+
|
|
664
|
+
if response.status_code == 202:
|
|
665
|
+
not_found: DashlogsNotFound = response.json()
|
|
666
|
+
reason = not_found.get("reason", "unknown")
|
|
667
|
+
if reason == "expired":
|
|
668
|
+
raise APIError(
|
|
669
|
+
"Logs have expired",
|
|
670
|
+
status_code=202,
|
|
671
|
+
details=f"Logs are deleted after {not_found.get('retention_days', '?')} days",
|
|
672
|
+
)
|
|
673
|
+
raise APIError(
|
|
674
|
+
"Logs not available yet",
|
|
675
|
+
status_code=202,
|
|
676
|
+
details="Build may still be in progress",
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
token_refreshed = await self._refresh_token_if_needed(response)
|
|
680
|
+
if token_refreshed:
|
|
681
|
+
retry_count += 1
|
|
682
|
+
else:
|
|
683
|
+
break
|
|
684
|
+
|
|
685
|
+
raise _handle_error_response(response, f"get {log_type} logs")
|
|
686
|
+
except httpx.RequestError as e:
|
|
687
|
+
raise NetworkError(f"Failed to get {log_type} logs", str(e)) from e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|