@@ -208,6 +208,23 @@ class MetricPoint:
208208 timestamp : int = 0
209209
210210
211+ @dataclass
212+ class BranchInfo :
213+ """Information about a copy-on-write topic branch (M5).
214+
215+ Attributes:
216+ name: Branch name.
217+ base_topic: The base topic this branch forks from.
218+ state: Branch state (active, discarded, merged).
219+ created_at: Creation timestamp in milliseconds since epoch.
220+ """
221+
222+ name : str
223+ base_topic : str
224+ state : str = "active"
225+ created_at : int = 0
226+
227+
211228class Admin :
212229 """Administrative operations for Streamline.
213230
@@ -399,6 +416,51 @@ def _sync_get():
399416 return json .loads (resp .read ())
400417 return await asyncio .to_thread (_sync_get )
401418
419+ async def _http_post (self , path : str , body : Any ) -> Any :
420+ """Make an HTTP POST request to the Streamline REST API."""
421+ http_url = getattr (self ._client_config , "http_url" , "http://localhost:9094" )
422+ url = f"{ http_url } { path } "
423+
424+ if HAS_AIOHTTP :
425+ async with aiohttp .ClientSession () as session :
426+ async with session .post (url , json = body , timeout = aiohttp .ClientTimeout (total = 10 )) as resp :
427+ if resp .status == 404 :
428+ raise TopicError (f"Not found: { path } " )
429+ if resp .status not in (200 , 201 ):
430+ text = await resp .text ()
431+ raise TopicError (f"HTTP { resp .status } : { text } " )
432+ return await resp .json ()
433+ else :
434+ import urllib .request
435+ payload = json .dumps (body ).encode ("utf-8" )
436+ req = urllib .request .Request (url , data = payload , method = "POST" )
437+ req .add_header ("Content-Type" , "application/json" )
438+ def _sync_post ():
439+ with urllib .request .urlopen (req , timeout = 10 ) as resp :
440+ return json .loads (resp .read ())
441+ return await asyncio .to_thread (_sync_post )
442+
443+ async def _http_delete (self , path : str ) -> None :
444+ """Make an HTTP DELETE request to the Streamline REST API."""
445+ http_url = getattr (self ._client_config , "http_url" , "http://localhost:9094" )
446+ url = f"{ http_url } { path } "
447+
448+ if HAS_AIOHTTP :
449+ async with aiohttp .ClientSession () as session :
450+ async with session .delete (url , timeout = aiohttp .ClientTimeout (total = 10 )) as resp :
451+ if resp .status == 404 :
452+ raise TopicError (f"Not found: { path } " )
453+ if resp .status >= 300 :
454+ text = await resp .text ()
455+ raise TopicError (f"HTTP { resp .status } : { text } " )
456+ else :
457+ import urllib .request
458+ req = urllib .request .Request (url , method = "DELETE" )
459+ def _sync_delete ():
460+ with urllib .request .urlopen (req , timeout = 10 ) as resp :
461+ pass
462+ await asyncio .to_thread (_sync_delete )
463+
402464 async def list_consumer_groups (self ) -> List [str ]:
403465 """List all consumer groups.
404466
@@ -609,6 +671,62 @@ async def metrics_history(self) -> List[MetricPoint]:
609671 for m in data
610672 ]
611673
674+ async def create_branch (
675+ self , name : str , base_topic : str , base_offsets : Optional [Dict [int , int ]] = None
676+ ) -> "BranchInfo" :
677+ """Create a copy-on-write branch of a topic (M5).
678+
679+ Args:
680+ name: Branch name.
681+ base_topic: Topic to branch from.
682+ base_offsets: Per-partition base offsets (partition -> offset).
683+
684+ Returns:
685+ BranchInfo for the newly created branch.
686+ """
687+ body : Dict [str , Any ] = {"name" : name , "base_topic" : base_topic }
688+ if base_offsets :
689+ body ["base_offsets" ] = base_offsets
690+ data = await self ._http_post ("/v1/branches" , body )
691+ return BranchInfo (
692+ name = data .get ("name" , name ),
693+ base_topic = data .get ("base_topic" , base_topic ),
694+ state = data .get ("state" , "active" ),
695+ created_at = int (data .get ("created_at" , 0 )),
696+ )
697+
698+ async def list_branches (self , topic : Optional [str ] = None ) -> List ["BranchInfo" ]:
699+ """List copy-on-write topic branches (M5).
700+
701+ Args:
702+ topic: Filter by base topic (optional).
703+
704+ Returns:
705+ List of BranchInfo objects.
706+ """
707+ path = "/v1/branches"
708+ if topic :
709+ path += f"?topic={ topic } "
710+ data = await self ._http_get (path )
711+ items = data if isinstance (data , list ) else data .get ("items" , [])
712+ return [
713+ BranchInfo (
714+ name = b .get ("name" , "" ),
715+ base_topic = b .get ("base_topic" , "" ),
716+ state = b .get ("state" , "active" ),
717+ created_at = int (b .get ("created_at" , 0 )),
718+ )
719+ for b in items
720+ ]
721+
722+ async def discard_branch (self , branch_id : str ) -> None :
723+ """Discard (delete) a copy-on-write topic branch (M5).
724+
725+ Args:
726+ branch_id: Branch identifier.
727+ """
728+ await self ._http_delete (f"/v1/branches/{ branch_id } " )
729+
612730 async def __aenter__ (self ) -> "Admin" :
613731 """Enter async context manager."""
614732 await self .start ()
0 commit comments