Pyvider uses decorators to register components with the hub-based discovery system. This guide shows how to use each decorator type correctly.
🤖 AI-Generated Content
This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.
frompyvider.providersimportregister_provider,BaseProvider,ProviderMetadatafrompyvider.schemaimports_provider,a_str,PvsSchemaimportattrs@attrs.defineclassMyCloudConfig:"""Provider runtime configuration."""api_key:strregion:str="us-east-1"timeout:int=30@register_provider("mycloud")classMyCloudProvider(BaseProvider):"""MyCloud infrastructure provider."""def__init__(self):super().__init__(metadata=ProviderMetadata(name="mycloud",version="1.0.0",protocol_version="6"))self.api_client=Noneself.provider_config:MyCloudConfig|None=Nonedef_build_schema(self)->PvsSchema:"""Define provider configuration schema."""returns_provider({"api_key":a_str(required=True,sensitive=True,description="API key for authentication"),"region":a_str(default="us-east-1",description="Default region for resources"),"timeout":a_num(default=30,description="API request timeout in seconds"),})asyncdefconfigure(self,config:dict)->None:"""Configure the provider with user settings."""awaitsuper().configure(config)# Convert config dict to attrs instanceself.provider_config=MyCloudConfig(api_key=config["api_key"],region=config.get("region","us-east-1"),timeout=config.get("timeout",30),)# Initialize API clientself.api_client=MyCloudAPIClient(api_key=self.provider_config.api_key,region=self.provider_config.region,timeout=self.provider_config.timeout,)
frompyvider.resourcesimportregister_resource,BaseResourcefrompyvider.resources.contextimportResourceContextfrompyvider.schemaimports_resource,a_str,a_num,PvsSchemaimportattrs@attrs.defineclassInstanceConfig:"""Resource configuration from user."""name:strsize:str="t2.micro"ami:str|None=None@attrs.defineclassInstanceState:"""Resource state managed by provider."""id:strname:strsize:strami:strpublic_ip:strstatus:str@register_resource("instance")classInstance(BaseResource):"""Cloud compute instance resource."""config_class=InstanceConfigstate_class=InstanceState@classmethoddefget_schema(cls)->PvsSchema:"""Define Terraform schema."""returns_resource({# User inputs"name":a_str(required=True,description="Instance name"),"size":a_str(default="t2.micro",description="Instance size"),"ami":a_str(description="AMI ID (defaults to latest)"),# Provider outputs"id":a_str(computed=True,description="Instance ID"),"public_ip":a_str(computed=True,description="Public IP address"),"status":a_str(computed=True,description="Instance status"),})asyncdef_validate_config(self,config:InstanceConfig)->list[str]:"""Validate configuration."""errors=[]valid_sizes=["t2.micro","t2.small","t2.medium","t3.micro"]ifconfig.sizenotinvalid_sizes:errors.append(f"size must be one of: {', '.join(valid_sizes)}")returnerrorsasyncdefread(self,ctx:ResourceContext)->InstanceState|None:"""Refresh instance state from API."""ifnotctx.state:returnNone# Fetch from APIinstance=awaitself.api.get_instance(ctx.state.id)ifnotinstance:returnNone# Instance deletedreturnInstanceState(id=ctx.state.id,name=instance.name,size=instance.size,ami=instance.ami,public_ip=instance.public_ip,status=instance.status,)asyncdef_create_apply(self,ctx:ResourceContext)->tuple[InstanceState|None,None]:"""Create instance (apply phase)."""ifnotctx.config:returnNone,None# Create via APIinstance=awaitself.api.create_instance(name=ctx.config.name,size=ctx.config.size,ami=ctx.config.amior"ami-latest",)returnInstanceState(id=instance.id,name=instance.name,size=instance.size,ami=instance.ami,public_ip=instance.public_ip,status=instance.status,),Noneasyncdef_update_apply(self,ctx:ResourceContext)->tuple[InstanceState|None,None]:"""Update instance (apply phase)."""ifnotctx.configornotctx.state:returnNone,None# Update via APIinstance=awaitself.api.update_instance(id=ctx.state.id,name=ctx.config.name,size=ctx.config.size,)returnInstanceState(id=ctx.state.id,name=instance.name,size=instance.size,ami=ctx.state.ami,# AMI can't changepublic_ip=instance.public_ip,status=instance.status,),Noneasyncdef_delete_apply(self,ctx:ResourceContext)->None:"""Delete instance (apply phase)."""ifnotctx.state:returnawaitself.api.delete_instance(ctx.state.id)
frompyvider.data_sourcesimportregister_data_source,BaseDataSourcefrompyvider.schemaimports_data_source,a_str,a_list,PvsSchemaimportattrs@attrs.defineclassAMILookupConfig:"""Data source configuration (input)."""name_filter:strowner:str="amazon"@attrs.defineclassAMILookupData:"""Data source result (output)."""id:strami_id:strname:strdescription:strarchitecture:strcreation_date:str@register_data_source("ami")classAMILookup(BaseDataSource):"""Looks up AMI information."""config_class=AMILookupConfigdata_class=AMILookupData@classmethoddefget_schema(cls)->PvsSchema:"""Define data source schema."""returns_data_source({# Inputs"name_filter":a_str(required=True,description="AMI name filter pattern"),"owner":a_str(default="amazon",description="AMI owner"),# Outputs"id":a_str(computed=True,description="Data source ID"),"ami_id":a_str(computed=True,description="AMI ID"),"name":a_str(computed=True,description="AMI name"),"description":a_str(computed=True,description="AMI description"),"architecture":a_str(computed=True,description="Architecture"),"creation_date":a_str(computed=True,description="Creation date"),})asyncdefread(self,config:AMILookupConfig)->AMILookupData:"""Query API for AMI information."""# Search for AMIamis=awaitself.api.search_amis(name_filter=config.name_filter,owner=config.owner,)ifnotamis:raiseDataSourceError(f"No AMI found matching '{config.name_filter}'")# Return most recentami=amis[0]returnAMILookupData(id=ami.id,ami_id=ami.id,name=ami.name,description=ami.description,architecture=ami.architecture,creation_date=ami.creation_date,)
frompyvider.functionsimportregister_function,BaseFunctionfrompyvider.schemaimports_function,a_str,PvsSchemaimportbase64@register_function("base64_encode")classBase64EncodeFunction(BaseFunction):"""Encodes a string to base64."""@classmethoddefget_schema(cls)->PvsSchema:"""Define function signature."""returns_function(parameters=[a_str(description="String to encode"),],return_type=a_str(description="Base64-encoded string"),)asyncdefcall(self,input:str)->str:"""Execute the function."""encoded=base64.b64encode(input.encode()).decode()returnencoded
ephemeral"mycloud_token" "api"{scope="read:api"ttl=7200}resource"mycloud_api_call""data"{token=ephemeral.mycloud_token.api.token # Token is automatically revoked when no longer needed}
frompyvider.capabilitiesimportregister_capability,BaseCapabilityimporttime@register_capabilityclassCachingCapability(BaseCapability):"""Adds response caching to components."""def__init__(self,ttl:int=300):self.ttl=ttlself.cache={}asyncdefsetup(self):"""Initialize capability."""self.cache.clear()asyncdefteardown(self):"""Cleanup capability."""self.cache.clear()asyncdefget(self,key:str):"""Get from cache."""ifkeyinself.cache:entry=self.cache[key]iftime.time()<entry["expires"]:returnentry["value"]else:delself.cache[key]returnNoneasyncdefset(self,key:str,value):"""Set in cache."""self.cache[key]={"value":value,"expires":time.time()+self.ttl,}
frompyvider.capabilitiesimportuse_capability@register_resource("instance")@use_capability(CachingCapability(ttl=600))classInstance(BaseResource):"""Instance with caching."""pass
@register_resource("server")@use_capability(CachingCapability())@use_capability(MetricsCapability())classServer(BaseResource):"""Server with caching and metrics."""pass
importos# Only register in certain environmentsifos.getenv("ENABLE_BETA_FEATURES"):@register_resource("beta_feature")classBetaFeature(BaseResource):pass
# ✅ Good: One decorator per class@register_resource("instance")classInstance(BaseResource):pass# ❌ Bad: Don't register same class multiple times@register_resource("instance")@register_resource("server")# Don't do thisclassInstance(BaseResource):pass
# Don't register base classesclassMyBaseResource(BaseResource):"""Shared functionality for all resources."""asyncdef_common_validation(self):pass# Register concrete implementations@register_resource("instance")classInstance(MyBaseResource):pass@register_resource("database")classDatabase(MyBaseResource):pass
importpytest@pytest.fixturedefinstance_resource():"""Create instance resource for testing."""returnInstance()asyncdeftest_instance_creation(instance_resource):"""Test instance resource."""# Test implementationpass
# Inspect components registered in the hubfrompyvider.hubimporthub# List all registered resourcesresources=hub.get_components("resource")print(f"Registered resources: {list(resources.keys())}")# Get specific componentinstance_class=hub.get_component("resource","instance")
# Ensure decorator is applied and module is imported@register_resource("instance")# ← Decorator requiredclassInstance(BaseResource):pass# Ensure module is imported in __init__.pyfrom.resources.instanceimportInstance# ← Must import
# Don't register same name twice@register_resource("instance")classInstance1(BaseResource):pass@register_resource("instance")# ← Error: duplicate nameclassInstance2(BaseResource):pass