1414from typing import Any
1515
1616import httpx
17+ from flyte .syncify import syncify
1718
1819from ._config import Config , default_config
1920from ._errors import JiraAPIError , MissingCredentialsError
@@ -162,6 +163,27 @@ async def __aexit__(self, *exc_info: object) -> None:
162163 await self ._client .aclose ()
163164 self ._client = None
164165
166+ def __enter__ (self ) -> JiraClient :
167+ """Enter synchronously, for use with the blocking call form.
168+
169+ `__aenter__` runs on syncify's background loop — the same loop the
170+ syncified methods run on — so the underlying `httpx.AsyncClient` is
171+ created and used on a single loop.
172+ """
173+ return self ._enter_sync ()
174+
175+ def __exit__ (self , * exc_info : object ) -> None :
176+ self ._exit_sync ()
177+
178+ @syncify
179+ async def _enter_sync (self ) -> JiraClient :
180+ return await self .__aenter__ ()
181+
182+ @syncify
183+ async def _exit_sync (self ) -> None :
184+ await self .__aexit__ ()
185+
186+ @syncify
165187 async def request (
166188 self ,
167189 method : str ,
@@ -215,33 +237,38 @@ async def request(
215237 # reads
216238 # ------------------------------------------------------------------
217239
240+ @syncify
218241 async def get_myself (self ) -> dict [str , Any ]:
219242 """Return the authenticated user (`GET /myself`)."""
220- data = await self .request ("GET" , "/myself" )
243+ data = await self .request . aio ("GET" , "/myself" )
221244 return {
222245 "account_id" : data .get ("accountId" ),
223246 "display_name" : data .get ("displayName" ),
224247 "email" : data .get ("emailAddress" ),
225248 }
226249
250+ @syncify
227251 async def list_projects (self ) -> list [dict [str , Any ]]:
228252 """List projects visible to the authenticated user."""
229- data = await self .request ("GET" , "/project/search" , params = {"maxResults" : 50 })
253+ data = await self .request . aio ("GET" , "/project/search" , params = {"maxResults" : 50 })
230254 return [{"key" : p .get ("key" ), "name" : p .get ("name" ), "id" : p .get ("id" )} for p in data .get ("values" , [])]
231255
256+ @syncify
232257 async def get_issue (self , issue_key : str ) -> dict [str , Any ]:
233258 """Return a single issue by key (e.g. `PROJ-123`)."""
234- data = await self .request ("GET" , f"/issue/{ issue_key } " )
259+ data = await self .request . aio ("GET" , f"/issue/{ issue_key } " )
235260 return _simplify_issue (data , self .base_url )
236261
262+ @syncify
237263 async def search_issues (self , jql : str , max_results : int = 50 ) -> list [dict [str , Any ]]:
238264 """Search issues with JQL."""
239- data = await self .request ("GET" , "/search" , params = {"jql" : jql , "maxResults" : max_results })
265+ data = await self .request . aio ("GET" , "/search" , params = {"jql" : jql , "maxResults" : max_results })
240266 return [_simplify_issue (issue , self .base_url ) for issue in data .get ("issues" , [])]
241267
268+ @syncify
242269 async def list_comments (self , issue_key : str ) -> list [dict [str , Any ]]:
243270 """List comments on an issue."""
244- data = await self .request ("GET" , f"/issue/{ issue_key } /comment" )
271+ data = await self .request . aio ("GET" , f"/issue/{ issue_key } /comment" )
245272 return [
246273 {
247274 "id" : c .get ("id" ),
@@ -252,9 +279,10 @@ async def list_comments(self, issue_key: str) -> list[dict[str, Any]]:
252279 for c in data .get ("comments" , [])
253280 ]
254281
282+ @syncify
255283 async def list_transitions (self , issue_key : str ) -> list [dict [str , Any ]]:
256284 """List the transitions available for an issue."""
257- data = await self .request ("GET" , f"/issue/{ issue_key } /transitions" )
285+ data = await self .request . aio ("GET" , f"/issue/{ issue_key } /transitions" )
258286 return [
259287 {"id" : t .get ("id" ), "name" : t .get ("name" ), "to_status" : (t .get ("to" ) or {}).get ("name" )}
260288 for t in data .get ("transitions" , [])
@@ -264,6 +292,7 @@ async def list_transitions(self, issue_key: str) -> list[dict[str, Any]]:
264292 # writes
265293 # ------------------------------------------------------------------
266294
295+ @syncify
267296 async def create_issue (
268297 self ,
269298 project_key : str ,
@@ -299,9 +328,10 @@ async def create_issue(
299328 fields ["labels" ] = labels
300329 if extra_fields :
301330 fields .update (extra_fields )
302- data = await self .request ("POST" , "/issue" , json = {"fields" : fields })
331+ data = await self .request . aio ("POST" , "/issue" , json = {"fields" : fields })
303332 return {"key" : data .get ("key" ), "id" : data .get ("id" ), "url" : f"{ self .base_url } /browse/{ data .get ('key' )} " }
304333
334+ @syncify
305335 async def update_issue (
306336 self ,
307337 issue_key : str ,
@@ -320,22 +350,24 @@ async def update_issue(
320350 fields ["labels" ] = labels
321351 if extra_fields :
322352 fields .update (extra_fields )
323- await self .request ("PUT" , f"/issue/{ issue_key } " , json = {"fields" : fields })
353+ await self .request . aio ("PUT" , f"/issue/{ issue_key } " , json = {"fields" : fields })
324354 return {"key" : issue_key }
325355
356+ @syncify
326357 async def add_comment (self , issue_key : str , body : str ) -> dict [str , Any ]:
327358 """Add a comment to an issue."""
328- data = await self .request ("POST" , f"/issue/{ issue_key } /comment" , json = {"body" : _text_to_adf (body )})
359+ data = await self .request . aio ("POST" , f"/issue/{ issue_key } /comment" , json = {"body" : _text_to_adf (body )})
329360 return {"id" : data .get ("id" ), "created" : data .get ("created" )}
330361
362+ @syncify
331363 async def transition_issue (self , issue_key : str , transition : str ) -> dict [str , Any ]:
332364 """Transition an issue by transition name or id.
333365
334366 Looks up available transitions when a name is given; raises
335367 `JiraAPIError` when the name does not match.
336368 """
337369 if not transition .isdigit ():
338- transitions = await self .list_transitions (issue_key )
370+ transitions = await self .list_transitions . aio (issue_key )
339371 match = next ((t for t in transitions if t ["name" ].lower () == transition .lower ()), None )
340372 if match is None :
341373 available = ", " .join (t ["name" ] for t in transitions ) or "<none>"
@@ -345,12 +377,13 @@ async def transition_issue(self, issue_key: str, transition: str) -> dict[str, A
345377 transition_id = match ["id" ]
346378 else :
347379 transition_id = transition
348- await self .request ("POST" , f"/issue/{ issue_key } /transitions" , json = {"transition" : {"id" : transition_id }})
380+ await self .request . aio ("POST" , f"/issue/{ issue_key } /transitions" , json = {"transition" : {"id" : transition_id }})
349381 return {"key" : issue_key , "transition" : transition_id }
350382
383+ @syncify
351384 async def delete_issue (self , issue_key : str ) -> None :
352385 """Delete an issue permanently. Destructive and irreversible."""
353- await self .request ("DELETE" , f"/issue/{ issue_key } " )
386+ await self .request . aio ("DELETE" , f"/issue/{ issue_key } " )
354387
355388
356389def _safe_json (response : httpx .Response ) -> dict [str , Any ] | None :
0 commit comments