2026-04-17 18:42:08 -03:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, minimum-scale=1" >
2026-04-24 19:23:00 -03:00
< meta name = "generator" content = "pdoc3 0.11.6" >
2026-04-17 18:42:08 -03:00
< title > connpy.services.node_service API documentation< / title >
< meta name = "description" content = "" >
< link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity = "sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin >
< link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/typography.min.css" integrity = "sha512-Y1DYSb995BAfxobCkKepB1BqJJTPrOp3zPL74AWFugHHmmdcvO+C48WLrUOlhGMc0QG7AE3f7gmvvcrmX2fDoA==" crossorigin >
< link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css" crossorigin >
< style > : root { --highlight-color : #fe9 } . flex { display : flex !important } body { line-height : 1.5 em } # content { padding : 20 px } # sidebar { padding : 1.5 em ; overflow : hidden } # sidebar > * : last-child { margin-bottom : 2 cm } . http-server-breadcrumbs { font-size : 130 % ; margin : 0 0 15 px 0 } # footer { font-size : .75 em ; padding : 5 px 30 px ; border-top : 1 px solid #ddd ; text-align : right } # footer p { margin : 0 0 0 1 em ; display : inline-block } # footer p : last-child { margin-right : 30 px } h1 , h2 , h3 , h4 , h5 { font-weight : 300 } h1 { font-size : 2.5 em ; line-height : 1.1 em } h2 { font-size : 1.75 em ; margin : 2 em 0 .50 em 0 } h3 { font-size : 1.4 em ; margin : 1.6 em 0 .7 em 0 } h4 { margin : 0 ; font-size : 105 % } h1 : target , h2 : target , h3 : target , h4 : target , h5 : target , h6 : target { background : var ( - - highlight - color ) ; padding : .2 em 0 } a { color : #058 ; text-decoration : none ; transition : color .2 s ease-in-out } a : visited { color : #503 } a : hover { color : #b62 } . title code { font-weight : bold } h2 [ id ^ = "header-" ] { margin-top : 2 em } . ident { color : #900 ; font-weight : bold } pre code { font-size : .8 em ; line-height : 1.4 em ; padding : 1 em ; display : block } code { background : #f3f3f3 ; font-family : "DejaVu Sans Mono" , monospace ; padding : 1 px 4 px ; overflow-wrap : break-word } h1 code { background : transparent } pre { border-top : 1 px solid #ccc ; border-bottom : 1 px solid #ccc ; margin : 1 em 0 } # http-server-module-list { display : flex ; flex-flow : column } # http-server-module-list div { display : flex } # http-server-module-list dt { min-width : 10 % } # http-server-module-list p { margin-top : 0 } . toc ul , # index { list-style-type : none ; margin : 0 ; padding : 0 } # index code { background : transparent } # index h3 { border-bottom : 1 px solid #ddd } # index ul { padding : 0 } # index h4 { margin-top : .6 em ; font-weight : bold } @ media ( min-width : 200ex ) { # index . two-column { column-count : 2 } } @ media ( min-width : 300ex ) { # index . two-column { column-count : 3 } } dl { margin-bottom : 2 em } dl dl : last-child { margin-bottom : 4 em } dd { margin : 0 0 1 em 3 em } # header-classes + dl > dd { margin-bottom : 3 em } dd dd { margin-left : 2 em } dd p { margin : 10 px 0 } . name { background : #eee ; font-size : .85 em ; padding : 5 px 10 px ; display : inline-block ; min-width : 40 % } . name : hover { background : #e0e0e0 } dt : target . name { background : var ( - - highlight - color ) } . name > span : first-child { white-space : nowrap } . name . class > span : nth-child ( 2 ) { margin-left : .4 em } . inherited { color : #999 ; border-left : 5 px solid #eee ; padding-left : 1 em } . inheritance em { font-style : normal ; font-weight : bold } . desc h2 { font-weight : 400 ; font-size : 1.25 em } . desc h3 { font-size : 1 em } . desc dt code { background : inherit } . source > summary , . git-link-div { color : #666 ; text-align : right ; font-weight : 400 ; font-size : .8 em ; text-transform : uppercase } . source summary > * { white-space : nowrap ; cursor : pointer } . git-link { color : inherit ; margin-left : 1 em } . source pre { max-height : 500 px ; overflow : auto ; margin : 0 } . source pre code { font-size : 12 px ; overflow : visible ; min-width : max-content } . hlist { list-style : none } . hlist li { display : inline } . hlist li : after { content : ',\2002' } . hlist li : last-child : after { content : none } . hlist . hlist { display : inline ; padding-left : 1 em } img { max-width : 100 % } td { padding : 0 .5 em } . admonition { padding : .1 em 1 em ; margin : 1 em 0 } . admonition-title { font-weight : bold } . admonition . note , . admonition . info , . admonition . important { background : #aef } . admonition . todo , . admonition . versionadded , . admonition . tip , . admonition . hint { background : #dfd } . admonition . warning , . admonition . versionchanged , . admonition . deprecated { background : #fd4 } . admonition . error , . admonition . danger , . admonition . caution { background : lightpink } < / style >
< style media = "screen and (min-width: 700px)" > @ media screen and ( min-width : 700px ) { # sidebar { width : 30 % ; height : 100 vh ; overflow : auto ; position : sticky ; top : 0 } # content { width : 70 % ; max-width : 100 ch ; padding : 3 em 4 em ; border-left : 1 px solid #ddd } pre code { font-size : 1 em } . name { font-size : 1 em } main { display : flex ; flex-direction : row-reverse ; justify-content : flex-end } . toc ul ul , # index ul ul { padding-left : 1 em } . toc > ul > li { margin-top : .5 em } } < / style >
< style media = "print" > @ media print { # sidebar h1 { page-break-before : always } . source { display : none } } @ media print { * { background : transparent !important ; color : #000 !important ; box-shadow : none !important ; text-shadow : none !important } a [ href ] : after { content : " (" attr ( href ) ")" ; font-size : 90 % } a [ href ] [ title ] : after { content : none } abbr [ title ] : after { content : " (" attr ( title ) ")" } . ir a : after , a [ href ^ = "javascript:" ] : after , a [ href ^ = "#" ] : after { content : "" } pre , blockquote { border : 1 px solid #999 ; page-break-inside : avoid } thead { display : table-header-group } tr , img { page-break-inside : avoid } img { max-width : 100 % !important } @ page { margin : 0 . 5cm } p , h2 , h3 { orphans : 3 ; widows : 3 } h1 , h2 , h3 , h4 , h5 , h6 { page-break-after : avoid } } < / style >
< script defer src = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" integrity = "sha512-D9gUyxqja7hBtkWpPWGt9wfbfaMGVt9gnyCvYa+jojwwPHLCzUm5i8rpk7vD7wNee9bA35eYIjobYPaQuKS1MQ==" crossorigin > < / script >
< script > window . addEventListener ( 'DOMContentLoaded' , ( ) => {
hljs . configure ( { languages : [ 'bash' , 'css' , 'diff' , 'graphql' , 'ini' , 'javascript' , 'json' , 'plaintext' , 'python' , 'python-repl' , 'rust' , 'shell' , 'sql' , 'typescript' , 'xml' , 'yaml' ] } ) ;
hljs . highlightAll ( ) ;
/* Collapse source docstrings */
setTimeout ( ( ) => {
[ ... document . querySelectorAll ( '.hljs.language-python > .hljs-string' ) ]
. filter ( el => el . innerHTML . length > 200 && [ '"""' , "'''" ] . includes ( el . innerHTML . substring ( 0 , 3 ) ) )
. forEach ( el => {
let d = document . createElement ( 'details' ) ;
d . classList . add ( 'hljs-string' ) ;
d . innerHTML = '<summary>"""</summary>' + el . innerHTML . substring ( 3 ) ;
el . replaceWith ( d ) ;
} ) ;
} , 100 ) ;
} ) < / script >
< / head >
< body >
< main >
< article id = "content" >
< header >
< h1 class = "title" > Module < code > connpy.services.node_service< / code > < / h1 >
< / header >
< section id = "section-intro" >
< / section >
< section >
< / section >
< section >
< / section >
< section >
< / section >
< section >
< h2 class = "section-title" id = "header-classes" > Classes< / h2 >
< dl >
< dt id = "connpy.services.node_service.NodeService" > < code class = "flex name class" >
< span > class < span class = "ident" > NodeService< / span > < / span >
< span > (< / span > < span > config=None)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > class NodeService(BaseService):
def __init__(self, config=None):
super().__init__(config)
def list_nodes(self, filter_str=None, format_str=None):
" " " Return a listed filtered by regex match and formatted if needed." " "
nodes = self.config._getallnodes()
case_sensitive = self.config.config.get(" case" , False)
if filter_str:
flags = re.IGNORECASE if not case_sensitive else 0
nodes = [n for n in nodes if re.search(filter_str, n, flags)]
if not format_str:
return nodes
from .profile_service import ProfileService
profile_service = ProfileService(self.config)
formatted_nodes = []
for n_id in nodes:
# Use ProfileService to resolve profiles for dynamic formatting
details = self.config.getitem(n_id, extract=False)
if details:
details = profile_service.resolve_node_data(details)
name = n_id.split(" @" )[0]
location = n_id.partition(" @" )[2] or " root"
# Prepare context for .format() with all details
context = details.copy()
context.update({
" name" : name,
" NAME" : name.upper(),
" location" : location,
" LOCATION" : location.upper(),
})
# Add exploded uniques (id, folder, subfolder)
uniques = self.config._explode_unique(n_id)
if uniques:
context.update(uniques)
# Add uppercase versions of all keys for convenience
for k, v in list(context.items()):
if isinstance(v, str):
context[k.upper()] = v.upper()
try:
formatted_nodes.append(format_str.format(**context))
except (KeyError, IndexError, ValueError):
# Fallback to original string if format fails
formatted_nodes.append(n_id)
return formatted_nodes
def list_folders(self, filter_str=None):
" " " Return all unique folders, optionally filtered by regex." " "
folders = self.config._getallfolders()
case_sensitive = self.config.config.get(" case" , False)
if filter_str:
flags = re.IGNORECASE if not case_sensitive else 0
folders = [f for f in folders if re.search(filter_str, f, flags)]
return folders
def get_node_details(self, unique_id):
" " " Return full configuration dictionary for a specific node." " "
2026-04-24 19:23:00 -03:00
try:
details = self.config.getitem(unique_id)
if not details:
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
return details
except (KeyError, TypeError):
2026-04-17 18:42:08 -03:00
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
def explode_unique(self, unique_id):
" " " Explode a unique ID into a dictionary of its parts." " "
return self.config._explode_unique(unique_id)
def generate_cache(self, nodes=None, folders=None, profiles=None):
" " " Generate and update the internal nodes cache." " "
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)
2026-04-24 19:23:00 -03:00
def validate_parent_folder(self, unique_id):
" " " Check if parent folder exists for a given node unique ID." " "
node_folder = unique_id.partition(" @" )[2]
if node_folder:
parent_folder = f" @{node_folder}"
if parent_folder not in self.config._getallfolders():
raise NodeNotFoundError(f" Folder ' {parent_folder}' not found." )
2026-04-17 18:42:08 -03:00
def add_node(self, unique_id, data, is_folder=False):
" " " Logic for adding a new node or folder to configuration." " "
if not is_folder:
self._validate_node_name(unique_id)
all_nodes = self.config._getallnodes()
all_folders = self.config._getallfolders()
if is_folder:
if unique_id in all_folders:
raise NodeAlreadyExistsError(f" Folder ' {unique_id}' already exists." )
uniques = self.config._explode_unique(unique_id)
if not uniques:
raise InvalidConfigurationError(f" Invalid folder name ' {unique_id}' ." )
# Check if parent folder exists when creating a subfolder
if " subfolder" in uniques:
2026-04-24 19:23:00 -03:00
self.validate_parent_folder(unique_id)
2026-04-17 18:42:08 -03:00
self.config._folder_add(**uniques)
self.config._saveconfig(self.config.file)
else:
if unique_id in all_nodes:
raise NodeAlreadyExistsError(f" Node ' {unique_id}' already exists." )
# Check if parent folder exists when creating a node in a folder
2026-04-24 19:23:00 -03:00
self.validate_parent_folder(unique_id)
2026-04-17 18:42:08 -03:00
# Ensure ' id' is in data for config._connections_add
if " id" not in data:
uniques = self.config._explode_unique(unique_id)
if uniques and " id" in uniques:
data[" id" ] = uniques[" id" ]
self.config._connections_add(**data)
self.config._saveconfig(self.config.file)
def update_node(self, unique_id, data):
" " " Explicitly update an existing node." " "
all_nodes = self.config._getallnodes()
if unique_id not in all_nodes:
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
# Ensure ' id' is in data for config._connections_add
if " id" not in data:
uniques = self.config._explode_unique(unique_id)
if uniques:
data[" id" ] = uniques[" id" ]
# config._connections_add actually handles updates if ID exists correctly
self.config._connections_add(**data)
self.config._saveconfig(self.config.file)
def delete_node(self, unique_id, is_folder=False):
" " " Logic for deleting a node or folder." " "
if is_folder:
uniques = self.config._explode_unique(unique_id)
if not uniques:
raise NodeNotFoundError(f" Folder ' {unique_id}' not found or invalid." )
self.config._folder_del(**uniques)
else:
uniques = self.config._explode_unique(unique_id)
if not uniques:
raise NodeNotFoundError(f" Node ' {unique_id}' not found or invalid." )
self.config._connections_del(**uniques)
self.config._saveconfig(self.config.file)
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
" " " Interact with a node directly." " "
from connpy.core import node
from .profile_service import ProfileService
node_data = self.config.getitem(unique_id, extract=False)
if not node_data:
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
# Resolve profiles
profile_service = ProfileService(self.config)
resolved_data = profile_service.resolve_node_data(node_data)
n = node(unique_id, **resolved_data, config=self.config)
if sftp:
n.protocol = " sftp"
n.interact(debug=debug, logger=logger)
def move_node(self, src_id, dst_id, copy=False):
" " " Move or copy a node." " "
self._validate_node_name(dst_id)
node_data = self.config.getitem(src_id)
if not node_data:
raise NodeNotFoundError(f" Source node ' {src_id}' not found." )
if dst_id in self.config._getallnodes():
raise NodeAlreadyExistsError(f" Destination node ' {dst_id}' already exists." )
new_uniques = self.config._explode_unique(dst_id)
if not new_uniques:
raise InvalidConfigurationError(f" Invalid destination format ' {dst_id}' ." )
new_node_data = node_data.copy()
new_node_data.update(new_uniques)
self.config._connections_add(**new_node_data)
if not copy:
src_uniques = self.config._explode_unique(src_id)
self.config._connections_del(**src_uniques)
self.config._saveconfig(self.config.file)
def bulk_add(self, ids, hosts, common_data):
" " " Add multiple nodes with shared common configuration." " "
count = 0
all_nodes = self.config._getallnodes()
for i, uid in enumerate(ids):
if uid in all_nodes:
continue
try:
self._validate_node_name(uid)
except ReservedNameError:
# For bulk, we might want to just skip or log.
# CLI caller will handle if it wants to be strict.
continue
host = hosts[i] if i < len(hosts) else hosts[0]
uniques = self.config._explode_unique(uid)
if not uniques:
continue
node_data = common_data.copy()
node_data.pop(" ids" , None)
node_data.pop(" location" , None)
node_data.update(uniques)
node_data[" host" ] = host
node_data[" type" ] = " connection"
self.config._connections_add(**node_data)
count += 1
if count > 0:
self.config._saveconfig(self.config.file)
return count
def full_replace(self, connections, profiles):
" " " Replace all connections and profiles with new data." " "
self.config.connections = connections
self.config.profiles = profiles
self.config._saveconfig(self.config.file)
def get_inventory(self):
" " " Return a full snapshot of connections and profiles." " "
return {
" connections" : self.config.connections,
" profiles" : self.config.profiles
}< / code > < / pre >
< / details >
< div class = "desc" > < p > Base class for all connpy services, providing common configuration access.< / p >
< p > Initialize the service.< / p >
< h2 id = "args" > Args< / h2 >
< dl >
< dt > < strong > < code > config< / code > < / strong > < / dt >
< dd > An instance of configfile (or None to instantiate a new one/use global context).< / dd >
< / dl > < / div >
< h3 > Ancestors< / h3 >
< ul class = "hlist" >
< li > < a title = "connpy.services.base.BaseService" href = "base.html#connpy.services.base.BaseService" > BaseService< / a > < / li >
< / ul >
< h3 > Methods< / h3 >
< dl >
< dt id = "connpy.services.node_service.NodeService.add_node" > < code class = "name flex" >
< span > def < span class = "ident" > add_node< / span > < / span > (< span > self, unique_id, data, is_folder=False)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def add_node(self, unique_id, data, is_folder=False):
" " " Logic for adding a new node or folder to configuration." " "
if not is_folder:
self._validate_node_name(unique_id)
all_nodes = self.config._getallnodes()
all_folders = self.config._getallfolders()
if is_folder:
if unique_id in all_folders:
raise NodeAlreadyExistsError(f" Folder ' {unique_id}' already exists." )
uniques = self.config._explode_unique(unique_id)
if not uniques:
raise InvalidConfigurationError(f" Invalid folder name ' {unique_id}' ." )
# Check if parent folder exists when creating a subfolder
if " subfolder" in uniques:
2026-04-24 19:23:00 -03:00
self.validate_parent_folder(unique_id)
2026-04-17 18:42:08 -03:00
self.config._folder_add(**uniques)
self.config._saveconfig(self.config.file)
else:
if unique_id in all_nodes:
raise NodeAlreadyExistsError(f" Node ' {unique_id}' already exists." )
# Check if parent folder exists when creating a node in a folder
2026-04-24 19:23:00 -03:00
self.validate_parent_folder(unique_id)
2026-04-17 18:42:08 -03:00
# Ensure ' id' is in data for config._connections_add
if " id" not in data:
uniques = self.config._explode_unique(unique_id)
if uniques and " id" in uniques:
data[" id" ] = uniques[" id" ]
self.config._connections_add(**data)
self.config._saveconfig(self.config.file)< / code > < / pre >
< / details >
< div class = "desc" > < p > Logic for adding a new node or folder to configuration.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.bulk_add" > < code class = "name flex" >
< span > def < span class = "ident" > bulk_add< / span > < / span > (< span > self, ids, hosts, common_data)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def bulk_add(self, ids, hosts, common_data):
" " " Add multiple nodes with shared common configuration." " "
count = 0
all_nodes = self.config._getallnodes()
for i, uid in enumerate(ids):
if uid in all_nodes:
continue
try:
self._validate_node_name(uid)
except ReservedNameError:
# For bulk, we might want to just skip or log.
# CLI caller will handle if it wants to be strict.
continue
host = hosts[i] if i < len(hosts) else hosts[0]
uniques = self.config._explode_unique(uid)
if not uniques:
continue
node_data = common_data.copy()
node_data.pop(" ids" , None)
node_data.pop(" location" , None)
node_data.update(uniques)
node_data[" host" ] = host
node_data[" type" ] = " connection"
self.config._connections_add(**node_data)
count += 1
if count > 0:
self.config._saveconfig(self.config.file)
return count< / code > < / pre >
< / details >
< div class = "desc" > < p > Add multiple nodes with shared common configuration.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.connect_node" > < code class = "name flex" >
< span > def < span class = "ident" > connect_node< / span > < / span > (< span > self, unique_id, sftp=False, debug=False, logger=None)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
" " " Interact with a node directly." " "
from connpy.core import node
from .profile_service import ProfileService
node_data = self.config.getitem(unique_id, extract=False)
if not node_data:
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
# Resolve profiles
profile_service = ProfileService(self.config)
resolved_data = profile_service.resolve_node_data(node_data)
n = node(unique_id, **resolved_data, config=self.config)
if sftp:
n.protocol = " sftp"
n.interact(debug=debug, logger=logger)< / code > < / pre >
< / details >
< div class = "desc" > < p > Interact with a node directly.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.delete_node" > < code class = "name flex" >
< span > def < span class = "ident" > delete_node< / span > < / span > (< span > self, unique_id, is_folder=False)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def delete_node(self, unique_id, is_folder=False):
" " " Logic for deleting a node or folder." " "
if is_folder:
uniques = self.config._explode_unique(unique_id)
if not uniques:
raise NodeNotFoundError(f" Folder ' {unique_id}' not found or invalid." )
self.config._folder_del(**uniques)
else:
uniques = self.config._explode_unique(unique_id)
if not uniques:
raise NodeNotFoundError(f" Node ' {unique_id}' not found or invalid." )
self.config._connections_del(**uniques)
self.config._saveconfig(self.config.file)< / code > < / pre >
< / details >
< div class = "desc" > < p > Logic for deleting a node or folder.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.explode_unique" > < code class = "name flex" >
< span > def < span class = "ident" > explode_unique< / span > < / span > (< span > self, unique_id)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def explode_unique(self, unique_id):
" " " Explode a unique ID into a dictionary of its parts." " "
return self.config._explode_unique(unique_id)< / code > < / pre >
< / details >
< div class = "desc" > < p > Explode a unique ID into a dictionary of its parts.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.full_replace" > < code class = "name flex" >
< span > def < span class = "ident" > full_replace< / span > < / span > (< span > self, connections, profiles)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def full_replace(self, connections, profiles):
" " " Replace all connections and profiles with new data." " "
self.config.connections = connections
self.config.profiles = profiles
self.config._saveconfig(self.config.file)< / code > < / pre >
< / details >
< div class = "desc" > < p > Replace all connections and profiles with new data.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.generate_cache" > < code class = "name flex" >
< span > def < span class = "ident" > generate_cache< / span > < / span > (< span > self, nodes=None, folders=None, profiles=None)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def generate_cache(self, nodes=None, folders=None, profiles=None):
" " " Generate and update the internal nodes cache." " "
self.config._generate_nodes_cache(nodes=nodes, folders=folders, profiles=profiles)< / code > < / pre >
< / details >
< div class = "desc" > < p > Generate and update the internal nodes cache.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.get_inventory" > < code class = "name flex" >
< span > def < span class = "ident" > get_inventory< / span > < / span > (< span > self)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def get_inventory(self):
" " " Return a full snapshot of connections and profiles." " "
return {
" connections" : self.config.connections,
" profiles" : self.config.profiles
}< / code > < / pre >
< / details >
< div class = "desc" > < p > Return a full snapshot of connections and profiles.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.get_node_details" > < code class = "name flex" >
< span > def < span class = "ident" > get_node_details< / span > < / span > (< span > self, unique_id)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def get_node_details(self, unique_id):
" " " Return full configuration dictionary for a specific node." " "
2026-04-24 19:23:00 -03:00
try:
details = self.config.getitem(unique_id)
if not details:
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
return details
except (KeyError, TypeError):
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )< / code > < / pre >
2026-04-17 18:42:08 -03:00
< / details >
< div class = "desc" > < p > Return full configuration dictionary for a specific node.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.list_folders" > < code class = "name flex" >
< span > def < span class = "ident" > list_folders< / span > < / span > (< span > self, filter_str=None)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def list_folders(self, filter_str=None):
" " " Return all unique folders, optionally filtered by regex." " "
folders = self.config._getallfolders()
case_sensitive = self.config.config.get(" case" , False)
if filter_str:
flags = re.IGNORECASE if not case_sensitive else 0
folders = [f for f in folders if re.search(filter_str, f, flags)]
return folders< / code > < / pre >
< / details >
< div class = "desc" > < p > Return all unique folders, optionally filtered by regex.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.list_nodes" > < code class = "name flex" >
< span > def < span class = "ident" > list_nodes< / span > < / span > (< span > self, filter_str=None, format_str=None)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def list_nodes(self, filter_str=None, format_str=None):
" " " Return a listed filtered by regex match and formatted if needed." " "
nodes = self.config._getallnodes()
case_sensitive = self.config.config.get(" case" , False)
if filter_str:
flags = re.IGNORECASE if not case_sensitive else 0
nodes = [n for n in nodes if re.search(filter_str, n, flags)]
if not format_str:
return nodes
from .profile_service import ProfileService
profile_service = ProfileService(self.config)
formatted_nodes = []
for n_id in nodes:
# Use ProfileService to resolve profiles for dynamic formatting
details = self.config.getitem(n_id, extract=False)
if details:
details = profile_service.resolve_node_data(details)
name = n_id.split(" @" )[0]
location = n_id.partition(" @" )[2] or " root"
# Prepare context for .format() with all details
context = details.copy()
context.update({
" name" : name,
" NAME" : name.upper(),
" location" : location,
" LOCATION" : location.upper(),
})
# Add exploded uniques (id, folder, subfolder)
uniques = self.config._explode_unique(n_id)
if uniques:
context.update(uniques)
# Add uppercase versions of all keys for convenience
for k, v in list(context.items()):
if isinstance(v, str):
context[k.upper()] = v.upper()
try:
formatted_nodes.append(format_str.format(**context))
except (KeyError, IndexError, ValueError):
# Fallback to original string if format fails
formatted_nodes.append(n_id)
return formatted_nodes< / code > < / pre >
< / details >
< div class = "desc" > < p > Return a listed filtered by regex match and formatted if needed.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.move_node" > < code class = "name flex" >
< span > def < span class = "ident" > move_node< / span > < / span > (< span > self, src_id, dst_id, copy=False)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def move_node(self, src_id, dst_id, copy=False):
" " " Move or copy a node." " "
self._validate_node_name(dst_id)
node_data = self.config.getitem(src_id)
if not node_data:
raise NodeNotFoundError(f" Source node ' {src_id}' not found." )
if dst_id in self.config._getallnodes():
raise NodeAlreadyExistsError(f" Destination node ' {dst_id}' already exists." )
new_uniques = self.config._explode_unique(dst_id)
if not new_uniques:
raise InvalidConfigurationError(f" Invalid destination format ' {dst_id}' ." )
new_node_data = node_data.copy()
new_node_data.update(new_uniques)
self.config._connections_add(**new_node_data)
if not copy:
src_uniques = self.config._explode_unique(src_id)
self.config._connections_del(**src_uniques)
self.config._saveconfig(self.config.file)< / code > < / pre >
< / details >
< div class = "desc" > < p > Move or copy a node.< / p > < / div >
< / dd >
< dt id = "connpy.services.node_service.NodeService.update_node" > < code class = "name flex" >
< span > def < span class = "ident" > update_node< / span > < / span > (< span > self, unique_id, data)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def update_node(self, unique_id, data):
" " " Explicitly update an existing node." " "
all_nodes = self.config._getallnodes()
if unique_id not in all_nodes:
raise NodeNotFoundError(f" Node ' {unique_id}' not found." )
# Ensure ' id' is in data for config._connections_add
if " id" not in data:
uniques = self.config._explode_unique(unique_id)
if uniques:
data[" id" ] = uniques[" id" ]
# config._connections_add actually handles updates if ID exists correctly
self.config._connections_add(**data)
self.config._saveconfig(self.config.file)< / code > < / pre >
< / details >
< div class = "desc" > < p > Explicitly update an existing node.< / p > < / div >
< / dd >
2026-04-24 19:23:00 -03:00
< dt id = "connpy.services.node_service.NodeService.validate_parent_folder" > < code class = "name flex" >
< span > def < span class = "ident" > validate_parent_folder< / span > < / span > (< span > self, unique_id)< / span >
< / code > < / dt >
< dd >
< details class = "source" >
< summary >
< span > Expand source code< / span >
< / summary >
< pre > < code class = "python" > def validate_parent_folder(self, unique_id):
" " " Check if parent folder exists for a given node unique ID." " "
node_folder = unique_id.partition(" @" )[2]
if node_folder:
parent_folder = f" @{node_folder}"
if parent_folder not in self.config._getallfolders():
raise NodeNotFoundError(f" Folder ' {parent_folder}' not found." )< / code > < / pre >
< / details >
< div class = "desc" > < p > Check if parent folder exists for a given node unique ID.< / p > < / div >
< / dd >
2026-04-17 18:42:08 -03:00
< / dl >
< h3 > Inherited members< / h3 >
< ul class = "hlist" >
< li > < code > < b > < a title = "connpy.services.base.BaseService" href = "base.html#connpy.services.base.BaseService" > BaseService< / a > < / b > < / code > :
< ul class = "hlist" >
< li > < code > < a title = "connpy.services.base.BaseService.set_reserved_names" href = "base.html#connpy.services.base.BaseService.set_reserved_names" > set_reserved_names< / a > < / code > < / li >
< / ul >
< / li >
< / ul >
< / dd >
< / dl >
< / section >
< / article >
< nav id = "sidebar" >
< div class = "toc" >
< ul > < / ul >
< / div >
< ul id = "index" >
< li > < h3 > Super-module< / h3 >
< ul >
< li > < code > < a title = "connpy.services" href = "index.html" > connpy.services< / a > < / code > < / li >
< / ul >
< / li >
< li > < h3 > < a href = "#header-classes" > Classes< / a > < / h3 >
< ul >
< li >
< h4 > < code > < a title = "connpy.services.node_service.NodeService" href = "#connpy.services.node_service.NodeService" > NodeService< / a > < / code > < / h4 >
2026-04-24 19:23:00 -03:00
< ul class = "" >
2026-04-17 18:42:08 -03:00
< li > < code > < a title = "connpy.services.node_service.NodeService.add_node" href = "#connpy.services.node_service.NodeService.add_node" > add_node< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.bulk_add" href = "#connpy.services.node_service.NodeService.bulk_add" > bulk_add< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.connect_node" href = "#connpy.services.node_service.NodeService.connect_node" > connect_node< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.delete_node" href = "#connpy.services.node_service.NodeService.delete_node" > delete_node< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.explode_unique" href = "#connpy.services.node_service.NodeService.explode_unique" > explode_unique< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.full_replace" href = "#connpy.services.node_service.NodeService.full_replace" > full_replace< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.generate_cache" href = "#connpy.services.node_service.NodeService.generate_cache" > generate_cache< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.get_inventory" href = "#connpy.services.node_service.NodeService.get_inventory" > get_inventory< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.get_node_details" href = "#connpy.services.node_service.NodeService.get_node_details" > get_node_details< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.list_folders" href = "#connpy.services.node_service.NodeService.list_folders" > list_folders< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.list_nodes" href = "#connpy.services.node_service.NodeService.list_nodes" > list_nodes< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.move_node" href = "#connpy.services.node_service.NodeService.move_node" > move_node< / a > < / code > < / li >
< li > < code > < a title = "connpy.services.node_service.NodeService.update_node" href = "#connpy.services.node_service.NodeService.update_node" > update_node< / a > < / code > < / li >
2026-04-24 19:23:00 -03:00
< li > < code > < a title = "connpy.services.node_service.NodeService.validate_parent_folder" href = "#connpy.services.node_service.NodeService.validate_parent_folder" > validate_parent_folder< / a > < / code > < / li >
2026-04-17 18:42:08 -03:00
< / ul >
< / li >
< / ul >
< / li >
< / ul >
< / nav >
< / main >
< footer id = "footer" >
2026-04-24 19:23:00 -03:00
< p > Generated by < a href = "https://pdoc3.github.io/pdoc" title = "pdoc: Python API documentation generator" > < cite > pdoc< / cite > 0.11.6< / a > .< / p >
2026-04-17 18:42:08 -03:00
< / footer >
< / body >
< / html >