6161from .models import (
6262 AppMode ,
6363 AppModes ,
64- AppSearchResults ,
6564 BootstrapOutputDTO ,
6665 BuildOutputDTO ,
6766 BundleMetadata ,
68- ConfigureResult ,
6967 ContentItemV0 ,
7068 ContentItemV1 ,
7169 DeleteInputDTO ,
@@ -436,11 +434,6 @@ def python_settings(self) -> PyInfo:
436434 response = self ._server .handle_bad_response (response )
437435 return response
438436
439- def app_search (self , filters : Optional [Mapping [str , JsonData ]]) -> AppSearchResults :
440- response = cast (Union [AppSearchResults , HTTPResponse ], self .get ("applications" , query_params = filters ))
441- response = self ._server .handle_bad_response (response )
442- return response
443-
444437 def app_get (self , app_id : str ) -> ContentItemV0 :
445438 response = cast (Union [ContentItemV0 , HTTPResponse ], self .get ("applications/%s" % app_id ))
446439 response = self ._server .handle_bad_response (response )
@@ -450,11 +443,6 @@ def app_add_environment_vars(self, app_guid: str, env_vars: list[tuple[str, str]
450443 env_body = [dict (name = kv [0 ], value = kv [1 ]) for kv in env_vars ]
451444 return self .patch ("v1/content/%s/environment" % app_guid , body = env_body )
452445
453- def app_config (self , app_id : str ) -> ConfigureResult :
454- response = cast (Union [ConfigureResult , HTTPResponse ], self .get ("applications/%s/config" % app_id ))
455- response = self ._server .handle_bad_response (response )
456- return response
457-
458446 def is_app_failed_response (self , response : HTTPResponse | JsonData ) -> bool :
459447 return isinstance (response , HTTPResponse ) and response .status >= 500
460448
@@ -465,10 +453,13 @@ def app_access(self, app_guid: str) -> None:
465453 response = self ._do_request (method , path , None , None , 3 , {}, False )
466454
467455 if self .is_app_failed_response (response ):
456+ # Get content metadata to construct logs URL
457+ content = self .content_get (app_guid )
458+ logs_url = content ["dashboard_url" ] + "/logs"
468459 raise RSConnectException (
469460 "Could not access the deployed content. "
470461 + "The app might not have started successfully."
471- + f"\n \t For more information: { self . app_config ( app_guid ). get ( ' logs_url' ) } "
462+ + f"\n \t For more information: { logs_url } "
472463 )
473464
474465 def bundle_download (self , content_guid : str , bundle_id : str ) -> HTTPResponse :
@@ -479,8 +470,8 @@ def bundle_download(self, content_guid: str, bundle_id: str) -> HTTPResponse:
479470 response = self ._server .handle_bad_response (response , is_httpresponse = True )
480471 return response
481472
482- def content_search (self ) -> list [ContentItemV1 ]:
483- response = cast (Union [List [ContentItemV1 ], HTTPResponse ], self .get ("v1/content" ))
473+ def content_list (self , filters : Optional [ Mapping [ str , JsonData ]] = None ) -> list [ContentItemV1 ]:
474+ response = cast (Union [List [ContentItemV1 ], HTTPResponse ], self .get ("v1/content" , query_params = filters ))
484475 response = self ._server .handle_bad_response (response )
485476 return response
486477
@@ -489,6 +480,22 @@ def content_get(self, content_guid: str) -> ContentItemV1:
489480 response = self ._server .handle_bad_response (response )
490481 return response
491482
483+ def get_content_by_id (self , app_id : str ) -> ContentItemV1 :
484+ """
485+ Get content by ID, which can be either a numeric ID (legacy) or GUID.
486+
487+ :param app_id: Either a numeric ID (e.g., "1234") or GUID (e.g., "abc-def-123")
488+ :return: ContentItemV1 data
489+ """
490+ # Check if it looks like a GUID (contains hyphens)
491+ if "-" in str (app_id ):
492+ return self .content_get (app_id )
493+ else :
494+ # Legacy numeric ID - get v0 content first to get GUID
495+ # TODO: deprecation warning
496+ app_v0 = self .app_get (app_id )
497+ return self .content_get (app_v0 ["guid" ])
498+
492499 def content_create (self , name : str ) -> ContentItemV1 :
493500 response = cast (Union [ContentItemV1 , HTTPResponse ], self .post ("v1/content" , body = {"name" : name }))
494501 response = self ._server .handle_bad_response (response )
@@ -589,15 +596,8 @@ def deploy(
589596 else :
590597 # assume content exists. if it was deleted then Connect will raise an error
591598 try :
592- # app_id could be a numeric ID (legacy) or GUID. Try to get it as content.
593- # If it's a numeric ID, app_get will work; if GUID, content_get will work.
594- # We'll use content_get if it looks like a GUID (contains hyphens), otherwise app_get.
595- if "-" in str (app_id ):
596- app = self .content_get (app_id )
597- else :
598- # Legacy numeric ID - get v0 content and convert to use GUID
599- app_v0 = self .app_get (app_id )
600- app = self .content_get (app_v0 ["guid" ])
599+ # app_id could be a numeric ID (legacy) or GUID
600+ app = self .get_content_by_id (app_id )
601601 except RSConnectException as e :
602602 raise RSConnectException (f"{ e } Try setting the --new flag to overwrite the previous deployment." ) from e
603603
@@ -632,7 +632,7 @@ def download_bundle(self, content_guid: str, bundle_id: str) -> HTTPResponse:
632632 return results
633633
634634 def search_content (self ) -> list [ContentItemV1 ]:
635- results = self .content_search ()
635+ results = self .content_list ()
636636 return results
637637
638638 def get_content (self , content_guid : str ) -> ContentItemV1 :
@@ -1244,11 +1244,9 @@ def validate_app_mode(self, app_mode: AppMode):
12441244 # to get this from the remote.
12451245 if isinstance (self .remote_server , RSConnectServer ):
12461246 try :
1247- app = get_app_info (self .remote_server , app_id )
1248- # TODO: verify that this is correct. The previous code seemed
1249- # incorrect. It passed an arg to app.get(), which would have
1250- # been ignored.
1251- existing_app_mode = AppModes .get_by_ordinal (app ["app_mode" ], True )
1247+ with RSConnectClient (self .remote_server ) as client :
1248+ content = client .get_content_by_id (app_id )
1249+ existing_app_mode = AppModes .get_by_ordinal (content ["app_mode" ], True )
12521250 except RSConnectException as e :
12531251 raise RSConnectException (
12541252 f"{ e } Try setting the --new flag to overwrite the previous deployment."
@@ -1975,18 +1973,6 @@ def get_python_info(connect_server: Union[RSConnectServer, SPCSConnectServer]):
19751973 return result
19761974
19771975
1978- def get_app_info (connect_server : Union [RSConnectServer , SPCSConnectServer ], app_id : str ):
1979- """
1980- Return information about an application that has been created in Connect.
1981-
1982- :param connect_server: the Connect server information.
1983- :param app_id: the ID (numeric or GUID) of the application to get info for.
1984- :return: the Python installation information from Connect.
1985- """
1986- with RSConnectClient (connect_server ) as client :
1987- return client .app_get (app_id )
1988-
1989-
19901976def get_posit_app_info (server : PositServer , app_id : str ):
19911977 with PositClient (server ) as client :
19921978 if isinstance (server , ShinyappsServer ):
@@ -1996,21 +1982,6 @@ def get_posit_app_info(server: PositServer, app_id: str):
19961982 return response ["source" ]
19971983
19981984
1999- def get_app_config (connect_server : Union [RSConnectServer , SPCSConnectServer ], app_id : str ):
2000- """
2001- Return the configuration information for an application that has been created
2002- in Connect.
2003-
2004- :param connect_server: the Connect server information.
2005- :param app_id: the ID (numeric or GUID) of the application to get the info for.
2006- :return: the Python installation information from Connect.
2007- """
2008- with RSConnectClient (connect_server ) as client :
2009- result = client .app_config (app_id )
2010- result = connect_server .handle_bad_response (result )
2011- return result
2012-
2013-
20141985def emit_task_log (
20151986 connect_server : Union [RSConnectServer , SPCSConnectServer ],
20161987 app_id : str ,
@@ -2041,80 +2012,12 @@ def emit_task_log(
20412012 with RSConnectClient (connect_server ) as client :
20422013 result = client .wait_for_task (task_id , log_callback , abort_func , timeout , poll_wait , raise_on_error )
20432014 result = connect_server .handle_bad_response (result )
2044- app_config = client . app_config ( app_id )
2045- connect_server . handle_bad_response ( app_config )
2046- app_url = app_config . get ( "config_url" )
2015+ # Get content (handles both numeric IDs and GUIDs )
2016+ content = client . get_content_by_id ( app_id )
2017+ app_url = content [ "dashboard_url" ]
20472018 return (app_url , * result )
20482019
20492020
2050- def retrieve_matching_apps (
2051- connect_server : Union [RSConnectServer , SPCSConnectServer ],
2052- filters : Optional [dict [str , str | int ]] = None ,
2053- limit : Optional [int ] = None ,
2054- mapping_function : Optional [Callable [[RSConnectClient , ContentItemV0 ], AbbreviatedAppItem | None ]] = None ,
2055- ) -> list [ContentItemV0 | AbbreviatedAppItem ]:
2056- """
2057- Retrieves all the app names that start with the given default name. The main
2058- point for this function is that it handles all the necessary paging logic.
2059-
2060- If a mapping function is provided, it must be a callable that accepts 2
2061- arguments. The first will be an `RSConnect` client, in the event extra calls
2062- per app are required. The second will be the current app. If the function
2063- returns None, then the app will be discarded and not appear in the result.
2064-
2065- :param connect_server: the Connect server information.
2066- :param filters: the filters to use for isolating the set of desired apps.
2067- :param limit: the maximum number of apps to retrieve. If this is None,
2068- then all matching apps are returned.
2069- :param mapping_function: an optional function that may transform or filter
2070- each app to return to something the caller wants.
2071- :return: the list of existing names that start with the proposed one.
2072- """
2073- page_size = 100
2074- result : list [ContentItemV0 | AbbreviatedAppItem ] = []
2075- search_filters : dict [str , str | int ] = filters .copy () if filters else {}
2076- search_filters ["count" ] = min (limit , page_size ) if limit else page_size
2077- total_returned = 0
2078- maximum = limit
2079- finished = False
2080-
2081- with RSConnectClient (connect_server ) as client :
2082- while not finished :
2083- response = client .app_search (search_filters )
2084-
2085- if not maximum :
2086- maximum = response ["total" ]
2087- else :
2088- maximum = min (maximum , response ["total" ])
2089-
2090- applications = response ["applications" ]
2091- returned = response ["count" ]
2092- delta = maximum - (total_returned + returned )
2093- # If more came back than we need, drop the rest.
2094- if delta < 0 :
2095- applications = applications [: abs (delta )]
2096- total_returned = total_returned + len (applications )
2097-
2098- if mapping_function :
2099- applications = [mapping_function (client , app ) for app in applications ]
2100- # Now filter out the None values that represent the apps the
2101- # function told us to drop.
2102- applications = [app for app in applications if app is not None ]
2103-
2104- result .extend (applications )
2105-
2106- if total_returned < maximum :
2107- search_filters = {
2108- "start" : total_returned ,
2109- "count" : page_size ,
2110- "cont" : response ["continuation" ],
2111- }
2112- else :
2113- finished = True
2114-
2115- return result
2116-
2117-
21182021class AbbreviatedAppItem (TypedDict ):
21192022 id : int
21202023 name : str
@@ -2124,78 +2027,6 @@ class AbbreviatedAppItem(TypedDict):
21242027 config_url : str
21252028
21262029
2127- def override_title_search (connect_server : Union [RSConnectServer , SPCSConnectServer ], app_id : str , app_title : str ):
2128- """
2129- Returns a list of abbreviated app data that contains apps with a title
2130- that matches the given one and/or the specific app noted by its ID.
2131-
2132- :param connect_server: the Connect server information.
2133- :param app_id: the ID of a specific app to look for, if any.
2134- :param app_title: the title to search for.
2135- :return: the list of matching apps, each trimmed to ID, name, title, mode
2136- URL and dashboard URL.
2137- """
2138-
2139- def map_app (app : ContentItemV0 , config : ConfigureResult ) -> AbbreviatedAppItem :
2140- """
2141- Creates the abbreviated data dictionary for the specified app and config
2142- information.
2143-
2144- :param app: the raw app data to start with.
2145- :param config: the configuration data to use.
2146- :return: the abbreviated app data dictionary.
2147- """
2148- return {
2149- "id" : app ["id" ],
2150- "name" : app ["name" ],
2151- "title" : app ["title" ],
2152- "app_mode" : AppModes .get_by_ordinal (app ["app_mode" ]).name (),
2153- "url" : app ["url" ],
2154- "config_url" : config ["config_url" ],
2155- }
2156-
2157- def mapping_filter (client : RSConnectClient , app : ContentItemV0 ) -> AbbreviatedAppItem | None :
2158- """
2159- Mapping/filter function for retrieving apps. We only keep apps
2160- that have an app mode of static or Jupyter notebook. The data
2161- for the apps we keep is an abbreviated subset.
2162-
2163- :param client: the client object to use for Posit Connect calls.
2164- :param app: the current app from Connect.
2165- :return: the abbreviated data for the app or None.
2166- """
2167- # Only keep apps that match our app modes.
2168- app_mode = AppModes .get_by_ordinal (app ["app_mode" ])
2169- if app_mode not in (AppModes .STATIC , AppModes .JUPYTER_NOTEBOOK ):
2170- return None
2171-
2172- config = client .app_config (str (app ["id" ]))
2173- config = connect_server .handle_bad_response (config )
2174-
2175- return map_app (app , config )
2176-
2177- apps = retrieve_matching_apps (
2178- connect_server ,
2179- filters = {"filter" : "min_role:editor" , "search" : app_title },
2180- mapping_function = mapping_filter ,
2181- limit = 5 ,
2182- )
2183-
2184- if app_id :
2185- found = next ((app for app in apps if app ["id" ] == app_id ), None )
2186-
2187- if not found :
2188- try :
2189- app = get_app_info (connect_server , app_id )
2190- mode = AppModes .get_by_ordinal (app ["app_mode" ])
2191- if mode in (AppModes .STATIC , AppModes .JUPYTER_NOTEBOOK ):
2192- apps .append (map_app (app , get_app_config (connect_server , app_id )))
2193- except RSConnectException :
2194- logger .debug ('Error getting info for previous app_id "%s", skipping.' , app_id )
2195-
2196- return apps
2197-
2198-
21992030def find_unique_name (remote_server : TargetableServer , name : str ):
22002031 """
22012032 Poll through existing apps to see if anything with a similar name exists.
@@ -2206,24 +2037,36 @@ def find_unique_name(remote_server: TargetableServer, name: str):
22062037 :return: the name, potentially with a suffixed number to guarantee uniqueness.
22072038 """
22082039 if isinstance (remote_server , (RSConnectServer , SPCSConnectServer )):
2209- existing_names = retrieve_matching_apps (
2210- remote_server ,
2211- filters = {"search" : name },
2212- mapping_function = lambda client , app : app ["name" ],
2213- )
2040+ # Use v1/content API with name query parameter
2041+ with RSConnectClient (remote_server ) as client :
2042+ results = client .content_list (filters = {"name" : name })
2043+
2044+ # If name exists, append suffix and try again
2045+ if len (results ) > 0 :
2046+ suffix = 1
2047+ test_name = "%s%d" % (name , suffix )
2048+ while True :
2049+ results = client .content_list (filters = {"name" : test_name })
2050+ if len (results ) == 0 :
2051+ return test_name
2052+ suffix = suffix + 1
2053+ test_name = "%s%d" % (name , suffix )
2054+
2055+ return name
2056+
22142057 elif isinstance (remote_server , ShinyappsServer ):
22152058 client = PositClient (remote_server )
22162059 existing_names = client .get_applications_like_name (name )
2217- else :
2218- # non-unique names are permitted in cloud
2219- return name
22202060
2221- if name in existing_names :
2222- suffix = 1
2223- test = "%s%d" % (name , suffix )
2224- while test in existing_names :
2225- suffix = suffix + 1
2061+ if name in existing_names :
2062+ suffix = 1
22262063 test = "%s%d" % (name , suffix )
2227- name = test
2064+ while test in existing_names :
2065+ suffix = suffix + 1
2066+ test = "%s%d" % (name , suffix )
2067+ name = test
22282068
2229- return name
2069+ return name
2070+ else :
2071+ # non-unique names are permitted in cloud
2072+ return name
0 commit comments