jira icon indicating copy to clipboard operation
jira copied to clipboard

Async client class?

Open karrukola opened this issue 2 years ago • 0 comments

Problem trying to solve

Hello, first of all thank you for the tool. I was wondering whether you would be open to add an asynchronous version of the client, for which the methods could be awaited. If so, I am happy to provide an initial contribution.

This would allow the users of this library to explicitly chose how to interact with the server (either synchronously or async).

The consumer code would then look like

async def _main():
    jira = ASyncJira(JIRA_URL, J_USR, J_PWD)

    srv_info = await jira.get_server_info()
    print(srv_info)

    # let's create two issues in parallel
    async with asyncio.TaskGroup() as task_group:
        for idx in range(2):
            task_group.create_task(
                jira.create_issue(
                    issue_data={
                        "project": {"key": "TEST"},
                        "summary": f"REST ye merry gentlemen - ({idx}/2)",
                        "issuetype": {"name": "Bug"},
                        "description": "Creating of an issue using project "
                        "keys and issue type names using the REST API.",
                    }
                )
            )


if __name__ == "__main__":
    asyncio.run(_main())

Possible solution(s)

Here is my quick idea of how the client class could be implemented. At its heart it makes the proper HTTP request and returns the JSON response. You have quite some code already between the JSON response and the return statement in the method that can be then re-used for both versions (sync vs async).

JsonType = Any

class ASyncJira:
    def __init__(self, url: str, usr: str, pwd: str) -> None:
        self.base_url = url
        self.api_url = urljoin(url, "rest/api/latest/")
        self.auth = (usr, pwd)
        self.client = AsyncClient(auth=(usr, pwd))

    async def _get_request(
        self, resource: str, params: Dict[str, str | None | int] | None = None
    ) -> JsonType:
        resp = await self.client.get(
            url=urljoin(self.api_url, resource),
            auth=self.auth,
            params=params,
        )
        resp.raise_for_status()
        return resp.json()

    async def _post_request(self, resource: str, json_content: JsonType) -> JsonType:
        resp = await self.client.post(
            url=urljoin(self.api_url, resource),
            auth=self.auth,
            json=json_content,
        )
        resp.raise_for_status()
        return resp.json()

    async def get_server_info(self) -> JsonType:
        """Retrieve server info.

        :return: output of the API.
        :rtype: JsonType
        """
        return await self._get_request("serverInfo")

    async def search_issues(
        self, jql: str, fields: str | None = None, max_results: int | None = None
    ):
        resp = await self._get_request(
            "search",
            params={"jql": jql, "fields": fields, "maxResults": max_results},
        )
        return resp["issues"]


    async def create_issue(self, issue_data):
        issue = await self._post_request(
            "issue",
            {
                "fields": issue_data,
            },
        )
        return issue


async def _print_jira_server_info(jira: ASyncJira):
    srv_info = await jira.get_server_info()
    print(srv_info)

Alternatives

the aiojira package seems to be the first-choice alternative, but that repository has been archived in November 2022.

Additional Context

I find this approach easier to reason about than defining the number of tasks and having this Jira library run the event loop itself. Moreover I understand that libraries should better leave the control of the loop of the application, that is outside of the scope of this repository.

The fundamental underlying dependency is HTTPX that promises to be compatible with Requests (I have not direct experience in porting from Requests to HTTPX) and allows for both sync and async calls.

As hinted earlier I do not see much code to be added. All of the parsing logic from the JSON response to the data structure that the method returns should be common; the main difference would be in the way the JSON is retrieved from the server.

karrukola avatar Jan 17 '23 10:01 karrukola