From d13dc93eee88013d660c533bb7da5245ecd61da2 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 11 May 2025 22:33:50 +0300 Subject: [PATCH 1/3] Confluence: Prepare dedicated module --- atlassian/bamboo.py | 1 + atlassian/bitbucket/base.py | 2 +- atlassian/{confluence.py => confluence/__init___.py} | 5 ++--- 3 files changed, 4 insertions(+), 4 deletions(-) rename atlassian/{confluence.py => confluence/__init___.py} (99%) diff --git a/atlassian/bamboo.py b/atlassian/bamboo.py index 66b60bbaf..a9fa51441 100755 --- a/atlassian/bamboo.py +++ b/atlassian/bamboo.py @@ -2,6 +2,7 @@ import logging from requests.exceptions import HTTPError + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/bitbucket/base.py b/atlassian/bitbucket/base.py index 4da72541d..750624076 100644 --- a/atlassian/bitbucket/base.py +++ b/atlassian/bitbucket/base.py @@ -3,9 +3,9 @@ import copy import re import sys - from datetime import datetime from pprint import PrettyPrinter + from ..rest_client import AtlassianRestAPI RE_TIMEZONE = re.compile(r"(\d{2}):(\d{2})$") diff --git a/atlassian/confluence.py b/atlassian/confluence/__init___.py similarity index 99% rename from atlassian/confluence.py rename to atlassian/confluence/__init___.py index e2f856b3e..d72da9e02 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence/__init___.py @@ -13,8 +13,7 @@ from requests import HTTPError from atlassian import utils - -from .errors import ( +from atlassian.errors import ( ApiConflictError, ApiError, ApiNotAcceptable, @@ -22,7 +21,7 @@ ApiPermissionError, ApiValueError, ) -from .rest_client import AtlassianRestAPI +from atlassian.rest_client import AtlassianRestAPI log = logging.getLogger(__name__) From c41e576d3403a973261440f7b7ee56eae5d587ba Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 11 May 2025 22:45:38 +0300 Subject: [PATCH 2/3] Fix tests --- atlassian/bitbucket/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/atlassian/bitbucket/__init__.py b/atlassian/bitbucket/__init__.py index b200f7c76..fa1a24cc0 100644 --- a/atlassian/bitbucket/__init__.py +++ b/atlassian/bitbucket/__init__.py @@ -162,7 +162,6 @@ def get_users_info(self, user_filter=None, start=0, limit=25): params["filter"] = user_filter return self._get_paged(url, params=params) - def get_current_license(self): """ Retrieves details about the current license, as well as the current status of the system with From 9a3305bf9b0bcb588478ede812ff20117eac32f3 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 24 Aug 2025 23:42:54 +0300 Subject: [PATCH 3/3] Tempo API wrap from API specs #446 --- atlassian/__init__.py | 3 + atlassian/tempo/__init__.py | 14 + atlassian/tempo/cloud/__init__.py | 232 ++++++++++ atlassian/tempo/cloud/base.py | 38 ++ atlassian/tempo/server/__init__.py | 78 ++++ atlassian/tempo/server/accounts.py | 38 ++ atlassian/tempo/server/base.py | 50 +++ atlassian/tempo/server/budgets.py | 42 ++ atlassian/tempo/server/events.py | 52 +++ atlassian/tempo/server/planner.py | 42 ++ atlassian/tempo/server/servlet.py | 46 ++ atlassian/tempo/server/teams.py | 50 +++ atlassian/tempo/server/timesheets.py | 54 +++ docs/tempo.rst | 620 +++++++++++++++++++++++++++ pytest.ini | 21 + tests/conftest.py | 51 +++ tests/test_tempo_cloud.py | 458 ++++++++++++++++++++ tests/test_tempo_cloud_pytest.py | 195 +++++++++ tests/test_tempo_server.py | 229 ++++++++++ tests/test_tempo_server_pytest.py | 234 ++++++++++ 20 files changed, 2547 insertions(+) create mode 100644 atlassian/tempo/__init__.py create mode 100644 atlassian/tempo/cloud/__init__.py create mode 100644 atlassian/tempo/cloud/base.py create mode 100644 atlassian/tempo/server/__init__.py create mode 100644 atlassian/tempo/server/accounts.py create mode 100644 atlassian/tempo/server/base.py create mode 100644 atlassian/tempo/server/budgets.py create mode 100644 atlassian/tempo/server/events.py create mode 100644 atlassian/tempo/server/planner.py create mode 100644 atlassian/tempo/server/servlet.py create mode 100644 atlassian/tempo/server/teams.py create mode 100644 atlassian/tempo/server/timesheets.py create mode 100644 docs/tempo.rst create mode 100644 pytest.ini create mode 100644 tests/conftest.py create mode 100644 tests/test_tempo_cloud.py create mode 100644 tests/test_tempo_cloud_pytest.py create mode 100644 tests/test_tempo_server.py create mode 100644 tests/test_tempo_server_pytest.py diff --git a/atlassian/__init__.py b/atlassian/__init__.py index 0b58404ce..13215a75a 100644 --- a/atlassian/__init__.py +++ b/atlassian/__init__.py @@ -16,6 +16,7 @@ from .portfolio import Portfolio from .service_desk import ServiceDesk from .service_desk import ServiceDesk as ServiceManagement +from .tempo import TempoCloud, TempoServer from .xray import Xray __all__ = [ @@ -35,4 +36,6 @@ "Insight", "Assets", "AssetsCloud", + "TempoCloud", + "TempoServer", ] diff --git a/atlassian/tempo/__init__.py b/atlassian/tempo/__init__.py new file mode 100644 index 000000000..60d263ca9 --- /dev/null +++ b/atlassian/tempo/__init__.py @@ -0,0 +1,14 @@ +# coding=utf-8 +""" +Tempo API client package for Atlassian Python API. + +This package provides both Cloud and Server implementations of the Tempo API. +""" + +from .cloud import Cloud as TempoCloud +from .server import Server as TempoServer + +__all__ = [ + "TempoCloud", + "TempoServer", +] diff --git a/atlassian/tempo/cloud/__init__.py b/atlassian/tempo/cloud/__init__.py new file mode 100644 index 000000000..41b3b6be1 --- /dev/null +++ b/atlassian/tempo/cloud/__init__.py @@ -0,0 +1,232 @@ +# coding=utf-8 + +from .base import TempoCloudBase + + +class Cloud(TempoCloudBase): + """ + Tempo Cloud REST API wrapper + """ + + def __init__(self, url="https://api.tempo.io/", *args, **kwargs): + # Set default API configuration for Tempo Cloud, but allow overrides + if "cloud" not in kwargs: + kwargs["cloud"] = True + if "api_version" not in kwargs: + kwargs["api_version"] = "1" + if "api_root" not in kwargs: + kwargs["api_root"] = "rest/tempo-timesheets/4" + super(Cloud, self).__init__(url, *args, **kwargs) + + # Account Management + def get_accounts(self, **kwargs): + """Get all accounts.""" + return self.get("accounts", **kwargs) + + def get_account(self, account_id, **kwargs): + """Get account by ID.""" + return self.get(f"accounts/{account_id}", **kwargs) + + def create_account(self, data, **kwargs): + """Create a new account.""" + return self.post("accounts", data=data, **kwargs) + + def update_account(self, account_id, data, **kwargs): + """Update an existing account.""" + return self.put(f"accounts/{account_id}", data=data, **kwargs) + + def delete_account(self, account_id, **kwargs): + """Delete an account.""" + return self.delete(f"accounts/{account_id}", **kwargs) + + # Worklog Management + def get_worklogs(self, **kwargs): + """Get all worklogs.""" + return self.get("worklogs", **kwargs) + + def get_worklog(self, worklog_id, **kwargs): + """Get worklog by ID.""" + return self.get(f"worklogs/{worklog_id}", **kwargs) + + def create_worklog(self, data, **kwargs): + """Create a new worklog.""" + return self.post("worklogs", data=data, **kwargs) + + def update_worklog(self, worklog_id, data, **kwargs): + """Update an existing worklog.""" + return self.put(f"worklogs/{worklog_id}", data=data, **kwargs) + + def delete_worklog(self, worklog_id, **kwargs): + """Delete a worklog.""" + return self.delete(f"worklogs/{worklog_id}", **kwargs) + + # Schedule Management + def get_schedules(self, **kwargs): + """Get all schedules.""" + return self.get("schedules", **kwargs) + + def get_schedule(self, schedule_id, **kwargs): + """Get schedule by ID.""" + return self.get(f"schedules/{schedule_id}", **kwargs) + + def create_schedule(self, data, **kwargs): + """Create a new schedule.""" + return self.post("schedules", data=data, **kwargs) + + def update_schedule(self, schedule_id, data, **kwargs): + """Update an existing schedule.""" + return self.put(f"schedules/{schedule_id}", data=data, **kwargs) + + def delete_schedule(self, schedule_id, **kwargs): + """Delete a schedule.""" + return self.delete(f"schedules/{schedule_id}", **kwargs) + + # User Management + def get_users(self, **kwargs): + """Get all users.""" + return self.get("users", **kwargs) + + def get_user(self, user_id, **kwargs): + """Get user by ID.""" + return self.get(f"users/{user_id}", **kwargs) + + def get_user_schedule(self, user_id, **kwargs): + """Get user's schedule.""" + return self.get(f"users/{user_id}/schedule", **kwargs) + + def get_user_worklogs(self, user_id, **kwargs): + """Get user's worklogs.""" + return self.get(f"users/{user_id}/worklogs", **kwargs) + + # Team Management + def get_teams(self, **kwargs): + """Get all teams.""" + return self.get("teams", **kwargs) + + def get_team(self, team_id, **kwargs): + """Get team by ID.""" + return self.get(f"teams/{team_id}", **kwargs) + + def create_team(self, data, **kwargs): + """Create a new team.""" + return self.post("teams", data=data, **kwargs) + + def update_team(self, team_id, data, **kwargs): + """Update an existing team.""" + return self.put(f"teams/{team_id}", data=data, **kwargs) + + def delete_team(self, team_id, **kwargs): + """Delete a team.""" + return self.delete(f"teams/{team_id}", **kwargs) + + def get_team_members(self, team_id, **kwargs): + """Get team members.""" + return self.get(f"teams/{team_id}/members", **kwargs) + + def add_team_member(self, team_id, user_id, **kwargs): + """Add member to team.""" + return self.post(f"teams/{team_id}/members", data={"userId": user_id}, **kwargs) + + def remove_team_member(self, team_id, user_id, **kwargs): + """Remove member from team.""" + return self.delete(f"teams/{team_id}/members/{user_id}", **kwargs) + + # Project Management + def get_projects(self, **kwargs): + """Get all projects.""" + return self.get("projects", **kwargs) + + def get_project(self, project_id, **kwargs): + """Get project by ID.""" + return self.get(f"projects/{project_id}", **kwargs) + + def get_project_worklogs(self, project_id, **kwargs): + """Get project worklogs.""" + return self.get(f"projects/{project_id}/worklogs", **kwargs) + + # Activity Management + def get_activities(self, **kwargs): + """Get all activities.""" + return self.get("activities", **kwargs) + + def get_activity(self, activity_id, **kwargs): + """Get activity by ID.""" + return self.get(f"activities/{activity_id}", **kwargs) + + def create_activity(self, data, **kwargs): + """Create a new activity.""" + return self.post("activities", data=data, **kwargs) + + def update_activity(self, activity_id, data, **kwargs): + """Update an existing activity.""" + return self.put(f"activities/{activity_id}", data=data, **kwargs) + + def delete_activity(self, activity_id, **kwargs): + """Delete an activity.""" + return self.delete(f"activities/{activity_id}", **kwargs) + + # Customer Management + def get_customers(self, **kwargs): + """Get all customers.""" + return self.get("customers", **kwargs) + + def get_customer(self, customer_id, **kwargs): + """Get customer by ID.""" + return self.get(f"customers/{customer_id}", **kwargs) + + def create_customer(self, data, **kwargs): + """Create a new customer.""" + return self.post("customers", data=data, **kwargs) + + def update_customer(self, customer_id, data, **kwargs): + """Update an existing customer.""" + return self.put(f"customers/{customer_id}", data=data, **kwargs) + + def delete_customer(self, customer_id, **kwargs): + """Delete a customer.""" + return self.delete(f"customers/{customer_id}", **kwargs) + + # Holiday Management + def get_holidays(self, **kwargs): + """Get all holidays.""" + return self.get("holidays", **kwargs) + + def get_holiday(self, holiday_id, **kwargs): + """Get holiday by ID.""" + return self.get(f"holidays/{holiday_id}", **kwargs) + + def create_holiday(self, data, **kwargs): + """Create a new holiday.""" + return self.post("holidays", data=data, **kwargs) + + def update_holiday(self, holiday_id, data, **kwargs): + """Update an existing holiday.""" + return self.put(f"holidays/{holiday_id}", data=data, **kwargs) + + def delete_holiday(self, holiday_id, **kwargs): + """Delete a holiday.""" + return self.delete(f"holidays/{holiday_id}", **kwargs) + + # Report Generation + def generate_report(self, report_type, params=None, **kwargs): + """Generate a report.""" + if params is None: + params = {} + return self.post(f"reports/{report_type}", data=params, **kwargs) + + def get_report_status(self, report_id, **kwargs): + """Get report generation status.""" + return self.get(f"reports/{report_id}/status", **kwargs) + + def download_report(self, report_id, **kwargs): + """Download a generated report.""" + return self.get(f"reports/{report_id}/download", **kwargs) + + # Utility Methods + def get_metadata(self, **kwargs): + """Get API metadata.""" + return self.get("metadata", **kwargs) + + def get_health(self, **kwargs): + """Get API health status.""" + return self.get("health", **kwargs) diff --git a/atlassian/tempo/cloud/base.py b/atlassian/tempo/cloud/base.py new file mode 100644 index 000000000..1a7d95cf9 --- /dev/null +++ b/atlassian/tempo/cloud/base.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" +Tempo Cloud API base class. +""" + +from ...rest_client import AtlassianRestAPI + + +class TempoCloudBase(AtlassianRestAPI): + """ + Base class for Tempo Cloud API operations. + """ + + def __init__(self, url, *args, **kwargs): + super(TempoCloudBase, self).__init__(url, *args, **kwargs) + + def _sub_url(self, url): + """ + Get the full url from a relative one. + + :param url: string: The sub url + :return: The absolute url + """ + return self.url_joiner(self.url, url) + + @property + def _new_session_args(self): + """ + Get the kwargs for new objects (session, root, version,...). + + :return: A dict with the kwargs for new objects + """ + return { + "session": self._session, + "cloud": self.cloud, + "api_root": self.api_root, + "api_version": self.api_version, + } diff --git a/atlassian/tempo/server/__init__.py b/atlassian/tempo/server/__init__.py new file mode 100644 index 000000000..2e066f740 --- /dev/null +++ b/atlassian/tempo/server/__init__.py @@ -0,0 +1,78 @@ +# coding=utf-8 + +from .base import TempoServerBase +from .accounts import Accounts +from .teams import Teams +from .planner import Planner +from .budgets import Budgets +from .timesheets import Timesheets +from .servlet import Servlet +from .events import Events + + +class Server(TempoServerBase): + """ + Tempo Server REST API wrapper + """ + + def __init__(self, url, *args, **kwargs): + # Set default API configuration for Tempo Server, but allow overrides + if "cloud" not in kwargs: + kwargs["cloud"] = False + if "api_version" not in kwargs: + kwargs["api_version"] = "1" + if "api_root" not in kwargs: + kwargs["api_root"] = "rest/tempo-core/1" + super(Server, self).__init__(url, *args, **kwargs) + + # Initialize specialized modules with reference to this instance + self.__accounts = Accounts(self._sub_url("accounts"), parent=self, **self._new_session_args) + self.__teams = Teams(self._sub_url("teams"), parent=self, **self._new_session_args) + self.__planner = Planner(self._sub_url("plans"), parent=self, **self._new_session_args) + self.__budgets = Budgets(self._sub_url("budgets"), parent=self, **self._new_session_args) + self.__timesheets = Timesheets(self._sub_url("timesheets"), parent=self, **self._new_session_args) + self.__servlet = Servlet(self._sub_url("worklogs"), parent=self, **self._new_session_args) + self.__events = Events(self._sub_url("events"), parent=self, **self._new_session_args) + + @property + def accounts(self): + """Property to access the accounts module.""" + return self.__accounts + + @property + def teams(self): + """Property to access the teams module.""" + return self.__teams + + @property + def planner(self): + """Property to access the planner module.""" + return self.__planner + + @property + def budgets(self): + """Property to access the budgets module.""" + return self.__budgets + + @property + def timesheets(self): + """Property to access the timesheets module.""" + return self.__timesheets + + @property + def servlet(self): + """Property to access the servlet module.""" + return self.__servlet + + @property + def events(self): + """Property to access the events module.""" + return self.__events + + def get_health(self, **kwargs): + """Get API health status.""" + return self.get("health", **kwargs) + + def get_metadata(self, **kwargs): + """Get API metadata.""" + return self.get("metadata", **kwargs) diff --git a/atlassian/tempo/server/accounts.py b/atlassian/tempo/server/accounts.py new file mode 100644 index 000000000..d40dd5a0d --- /dev/null +++ b/atlassian/tempo/server/accounts.py @@ -0,0 +1,38 @@ +# coding=utf-8 +""" +Tempo Server Accounts API module. +""" + +from .base import TempoServerBase + + +class Accounts(TempoServerBase): + """ + Tempo Server Accounts API client. + + Reference: https://www.tempo.io/server-api-documentation/accounts + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Accounts, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_accounts(self, **kwargs): + """Get all accounts.""" + return self.parent.get("", **kwargs) + + def get_account(self, account_id, **kwargs): + """Get account by ID.""" + return self.parent.get(f"{account_id}", **kwargs) + + def create_account(self, data, **kwargs): + """Create a new account.""" + return self.parent.post("", data=data, **kwargs) + + def update_account(self, account_id, data, **kwargs): + """Update an existing account.""" + return self.parent.put(f"{account_id}", data=data, **kwargs) + + def delete_account(self, account_id, **kwargs): + """Delete an account.""" + return self.parent.delete(f"{account_id}", **kwargs) diff --git a/atlassian/tempo/server/base.py b/atlassian/tempo/server/base.py new file mode 100644 index 000000000..56f00940f --- /dev/null +++ b/atlassian/tempo/server/base.py @@ -0,0 +1,50 @@ +# coding=utf-8 +""" +Tempo Server API base class. +""" + +from ...rest_client import AtlassianRestAPI + + +class TempoServerBase(AtlassianRestAPI): + """ + Base class for Tempo Server API operations. + """ + + def __init__(self, url, *args, **kwargs): + super(TempoServerBase, self).__init__(url, *args, **kwargs) + + def _sub_url(self, url): + """ + Get the full url from a relative one. + + :param url: string: The sub url + :return: The absolute url + """ + return self.url_joiner(self.url, url) + + @property + def _new_session_args(self): + """ + Get the kwargs for new objects (session, root, version,...). + + :return: A dict with the kwargs for new objects + """ + return { + "session": self._session, + "cloud": self.cloud, + "api_root": self.api_root, + "api_version": self.api_version, + } + + def _call_parent_method(self, method_name, *args, **kwargs): + """ + Call a method on the parent class. + + :param method_name: The name of the method to call + :param args: Arguments to pass to the method + :param kwargs: Keyword arguments to pass to the method + :return: The result of the method call + """ + method = getattr(super(), method_name) + return method(*args, **kwargs) diff --git a/atlassian/tempo/server/budgets.py b/atlassian/tempo/server/budgets.py new file mode 100644 index 000000000..5e0ada942 --- /dev/null +++ b/atlassian/tempo/server/budgets.py @@ -0,0 +1,42 @@ +# coding=utf-8 +""" +Tempo Server Budgets API module. +""" + +from .base import TempoServerBase + + +class Budgets(TempoServerBase): + """ + Tempo Server Budgets API client. + + Reference: https://www.tempo.io/server-api-documentation/budgets + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Budgets, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_budgets(self, **kwargs): + """Get all budgets.""" + return self.parent.get("", **kwargs) + + def get_budget(self, budget_id, **kwargs): + """Get budget by ID.""" + return self.parent.get(f"{budget_id}", **kwargs) + + def create_budget(self, data, **kwargs): + """Create a new budget.""" + return self.parent.post("", data=data, **kwargs) + + def update_budget(self, budget_id, data, **kwargs): + """Update an existing budget.""" + return self.parent.put(f"{budget_id}", data=data, **kwargs) + + def delete_budget(self, budget_id, **kwargs): + """Delete a budget.""" + return self.parent.delete(f"{budget_id}", **kwargs) + + def get_budget_allocations(self, budget_id, **kwargs): + """Get budget allocations.""" + return self.parent.get(f"{budget_id}/allocations", **kwargs) diff --git a/atlassian/tempo/server/events.py b/atlassian/tempo/server/events.py new file mode 100644 index 000000000..756dd640d --- /dev/null +++ b/atlassian/tempo/server/events.py @@ -0,0 +1,52 @@ +# coding=utf-8 +""" +Tempo Server Events API module. +""" + +from .base import TempoServerBase + + +class Events(TempoServerBase): + """ + Tempo Server Events API client. + + Reference: + - https://github.com/tempo-io/tempo-events-example/blob/master/README.md + - https://github.com/tempo-io/tempo-client-events/blob/master/README.md + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Events, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_events(self, **kwargs): + """Get all events.""" + return self.parent.get("", **kwargs) + + def get_event(self, event_id, **kwargs): + """Get event by ID.""" + return self.parent.get(f"{event_id}", **kwargs) + + def create_event(self, data, **kwargs): + """Create a new event.""" + return self.parent.post("", data=data, **kwargs) + + def update_event(self, event_id, data, **kwargs): + """Update an existing event.""" + return self.parent.put(f"{event_id}", data=data, **kwargs) + + def delete_event(self, event_id, **kwargs): + """Delete an event.""" + return self.parent.delete(f"{event_id}", **kwargs) + + def get_event_subscriptions(self, **kwargs): + """Get event subscriptions.""" + return self.parent.get("subscriptions", **kwargs) + + def create_event_subscription(self, data, **kwargs): + """Create a new event subscription.""" + return self.parent.post("subscriptions", data=data, **kwargs) + + def delete_event_subscription(self, subscription_id, **kwargs): + """Delete an event subscription.""" + return self.parent.delete(f"subscriptions/{subscription_id}", **kwargs) diff --git a/atlassian/tempo/server/planner.py b/atlassian/tempo/server/planner.py new file mode 100644 index 000000000..1c70a0412 --- /dev/null +++ b/atlassian/tempo/server/planner.py @@ -0,0 +1,42 @@ +# coding=utf-8 +""" +Tempo Server Planner API module. +""" + +from .base import TempoServerBase + + +class Planner(TempoServerBase): + """ + Tempo Server Planner API client. + + Reference: https://www.tempo.io/server-api-documentation/planner + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Planner, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_plans(self, **kwargs): + """Get all plans.""" + return self.parent.get("", **kwargs) + + def get_plan(self, plan_id, **kwargs): + """Get plan by ID.""" + return self.parent.get(f"{plan_id}", **kwargs) + + def create_plan(self, data, **kwargs): + """Create a new plan.""" + return self.parent.post("", data=data, **kwargs) + + def update_plan(self, plan_id, data, **kwargs): + """Update an existing plan.""" + return self.parent.put(f"{plan_id}", data=data, **kwargs) + + def delete_plan(self, plan_id, **kwargs): + """Delete a plan.""" + return self.parent.delete(f"{plan_id}", **kwargs) + + def get_plan_assignments(self, plan_id, **kwargs): + """Get plan assignments.""" + return self.parent.get(f"{plan_id}/assignments", **kwargs) diff --git a/atlassian/tempo/server/servlet.py b/atlassian/tempo/server/servlet.py new file mode 100644 index 000000000..314e7a6e9 --- /dev/null +++ b/atlassian/tempo/server/servlet.py @@ -0,0 +1,46 @@ +# coding=utf-8 +""" +Tempo Server Servlet API module. +""" + +from .base import TempoServerBase + + +class Servlet(TempoServerBase): + """ + Tempo Server Servlet API client. + + Reference: https://www.tempo.io/server-api-documentation/servlet + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Servlet, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_worklogs(self, **kwargs): + """Get all worklogs.""" + return self.parent.get("", **kwargs) + + def get_worklog(self, worklog_id, **kwargs): + """Get worklog by ID.""" + return self.parent.get(f"{worklog_id}", **kwargs) + + def create_worklog(self, data, **kwargs): + """Create a new worklog.""" + return self.parent.post("", data=data, **kwargs) + + def update_worklog(self, worklog_id, data, **kwargs): + """Update an existing worklog.""" + return self.parent.put(f"{worklog_id}", data=data, **kwargs) + + def delete_worklog(self, worklog_id, **kwargs): + """Delete a worklog.""" + return self.parent.delete(f"{worklog_id}", **kwargs) + + def get_worklog_attributes(self, worklog_id, **kwargs): + """Get worklog attributes.""" + return self.parent.get(f"{worklog_id}/attributes", **kwargs) + + def update_worklog_attributes(self, worklog_id, data, **kwargs): + """Update worklog attributes.""" + return self.parent.put(f"{worklog_id}/attributes", data=data, **kwargs) diff --git a/atlassian/tempo/server/teams.py b/atlassian/tempo/server/teams.py new file mode 100644 index 000000000..37cc4449f --- /dev/null +++ b/atlassian/tempo/server/teams.py @@ -0,0 +1,50 @@ +# coding=utf-8 +""" +Tempo Server Teams API module. +""" + +from .base import TempoServerBase + + +class Teams(TempoServerBase): + """ + Tempo Server Teams API client. + + Reference: https://www.tempo.io/server-api-documentation/teams + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Teams, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_teams(self, **kwargs): + """Get all teams.""" + return self.parent.get("", **kwargs) + + def get_team(self, team_id, **kwargs): + """Get team by ID.""" + return self.parent.get(f"{team_id}", **kwargs) + + def create_team(self, data, **kwargs): + """Create a new team.""" + return self.parent.post("", data=data, **kwargs) + + def update_team(self, team_id, data, **kwargs): + """Update an existing team.""" + return self.parent.put(f"{team_id}", data=data, **kwargs) + + def delete_team(self, team_id, **kwargs): + """Delete a team.""" + return self.parent.delete(f"{team_id}", **kwargs) + + def get_team_members(self, team_id, **kwargs): + """Get team members.""" + return self.parent.get(f"{team_id}/members", **kwargs) + + def add_team_member(self, team_id, user_id, **kwargs): + """Add member to team.""" + return self.parent.post(f"{team_id}/members", data={"userId": user_id}, **kwargs) + + def remove_team_member(self, team_id, user_id, **kwargs): + """Remove member from team.""" + return self.parent.delete(f"{team_id}/members/{user_id}", **kwargs) diff --git a/atlassian/tempo/server/timesheets.py b/atlassian/tempo/server/timesheets.py new file mode 100644 index 000000000..d59205c26 --- /dev/null +++ b/atlassian/tempo/server/timesheets.py @@ -0,0 +1,54 @@ +# coding=utf-8 +""" +Tempo Server Timesheets API module. +""" + +from .base import TempoServerBase + + +class Timesheets(TempoServerBase): + """ + Tempo Server Timesheets API client. + + Reference: https://www.tempo.io/server-api-documentation/timesheets + """ + + def __init__(self, url, parent=None, *args, **kwargs): + super(Timesheets, self).__init__(url, *args, **kwargs) + self.parent = parent + + def get_timesheets(self, **kwargs): + """Get all timesheets.""" + return self.parent.get("", **kwargs) + + def get_timesheet(self, timesheet_id, **kwargs): + """Get timesheet by ID.""" + return self.parent.get(f"{timesheet_id}", **kwargs) + + def create_timesheet(self, data, **kwargs): + """Create a new timesheet.""" + return self.parent.post("", data=data, **kwargs) + + def update_timesheet(self, timesheet_id, data, **kwargs): + """Update an existing timesheet.""" + return self.parent.put(f"{timesheet_id}", data=data, **kwargs) + + def delete_timesheet(self, timesheet_id, **kwargs): + """Delete a timesheet.""" + return self.parent.delete(f"{timesheet_id}", **kwargs) + + def get_timesheet_entries(self, timesheet_id, **kwargs): + """Get timesheet entries.""" + return self.parent.get(f"{timesheet_id}/entries", **kwargs) + + def submit_timesheet(self, timesheet_id, **kwargs): + """Submit a timesheet for approval.""" + return self.parent.post(f"{timesheet_id}/submit", **kwargs) + + def approve_timesheet(self, timesheet_id, **kwargs): + """Approve a timesheet.""" + return self.parent.post(f"{timesheet_id}/approve", **kwargs) + + def reject_timesheet(self, timesheet_id, reason, **kwargs): + """Reject a timesheet.""" + return self.parent.post(f"{timesheet_id}/reject", data={"reason": reason}, **kwargs) diff --git a/docs/tempo.rst b/docs/tempo.rst new file mode 100644 index 000000000..920c1496b --- /dev/null +++ b/docs/tempo.rst @@ -0,0 +1,620 @@ +Tempo API +========= + +The Tempo API client provides access to both Tempo Cloud and Tempo Server APIs +within Atlassian instances. + +Overview +-------- + +This implementation provides two main client types: + +- **TempoCloud**: For Tempo Cloud instances (hosted by Atlassian) +- **TempoServer**: For Tempo Server instances (self-hosted) + +The Tempo Cloud client is based on the official OpenAPI specification, +while the Tempo Server client provides access to various server-side API +modules. + +Installation +------------ + +The Tempo clients are included with the main atlassian-python-api package: + +.. code-block:: python + + from atlassian import TempoCloud, TempoServer + +Tempo Cloud +----------- + +The Tempo Cloud client provides access to Tempo's cloud-based time tracking +and project management capabilities. + +Basic Usage +----------- +Initialize the Tempo Cloud client: + +.. code-block:: python + + tempo = TempoCloud( + url="https://your-domain.atlassian.net", + token="your-tempo-api-token", + cloud=True + ) + +### Authentication + +Tempo Cloud uses API tokens for authentication. Generate a token from your +Tempo Cloud settings: + +1. Go to your Tempo Cloud instance +2. Navigate to **Settings** → **Integrations** → **API Keys** +3. Create a new API key +4. Use the generated token in your client initialization + +### API Endpoints + +The Tempo Cloud client provides access to the following endpoints: + +#### Account Management + +.. code-block:: python + + # Get all accounts + accounts = tempo.get_accounts() + + # Get specific account + account = tempo.get_account(account_id) + + # Create new account + new_account = tempo.create_account({ + "name": "Client Project", + "key": "CLIENT", + "status": "ACTIVE" + }) + + # Update account + updated_account = tempo.update_account(account_id, { + "name": "Updated Project Name" + }) + + # Delete account + tempo.delete_account(account_id) + +Worklog Management +------------------ +.. code-block:: python + + # Get all worklogs + worklogs = tempo.get_worklogs() + + # Get specific worklog + worklog = tempo.get_worklog(worklog_id) + + # Create new worklog + new_worklog = tempo.create_worklog({ + "issueKey": "PROJ-123", + "timeSpentSeconds": 3600, # 1 hour + "dateCreated": "2024-01-15", + "description": "Development work" + }) + + # Update worklog + updated_worklog = tempo.update_worklog(worklog_id, { + "timeSpentSeconds": 7200 # 2 hours + }) + + # Delete worklog + tempo.delete_worklog(worklog_id) + +Schedule Management +------------------- +.. code-block:: python + + # Get all schedules + schedules = tempo.get_schedules() + + # Get specific schedule + schedule = tempo.get_schedule(schedule_id) + + # Create new schedule + new_schedule = tempo.create_schedule({ + "name": "Flexible Schedule", + "type": "FLEXIBLE" + }) + + # Update schedule + updated_schedule = tempo.update_schedule(schedule_id, { + "name": "Updated Schedule Name" + }) + + # Delete schedule + tempo.delete_schedule(schedule_id) + +User Management +--------------- +.. code-block:: python + + # Get all users + users = tempo.get_users() + + # Get specific user + user = tempo.get_user(user_id) + + # Get user's schedule + user_schedule = tempo.get_user_schedule(user_id) + + # Get user's worklogs + user_worklogs = tempo.get_user_worklogs(user_id) + +Team Management +--------------- +.. code-block:: python + + # Get all teams + teams = tempo.get_teams() + + # Get specific team + team = tempo.get_team(team_id) + + # Create new team + new_team = tempo.create_team({ + "name": "Development Team", + "description": "Software development team" + }) + + # Update team + updated_team = tempo.update_team(team_id, { + "name": "Updated Team Name" + }) + + # Delete team + tempo.delete_team(team_id) + + # Get team members + team_members = tempo.get_team_members(team_id) + + # Add member to team + tempo.add_team_member(team_id, user_id) + + # Remove member from team + tempo.remove_team_member(team_id, user_id) + +Project Management +------------------ +.. code-block:: python + + # Get all projects + projects = tempo.get_projects() + + # Get specific project + project = tempo.get_project(project_id) + + # Get project worklogs + project_worklogs = tempo.get_project_worklogs(project_id) + +Activity Management +------------------- +.. code-block:: python + + # Get all activities + activities = tempo.get_activities() + + # Get specific activity + activity = tempo.get_activity(activity_id) + + # Create new activity + new_activity = tempo.create_activity({ + "name": "Code Review", + "description": "Reviewing code changes and providing feedback" + }) + + # Update activity + updated_activity = tempo.update_activity(activity_id, { + "name": "Updated Activity Name" + }) + + # Delete activity + tempo.delete_activity(activity_id) + +Customer Management +------------------- +.. code-block:: python + + # Get all customers + customers = tempo.get_customers() + + # Get specific customer + customer = tempo.get_customer(customer_id) + + # Create new customer + new_customer = tempo.create_customer({ + "name": "Acme Corporation", + "description": "Enterprise software client" + }) + + # Update customer + updated_customer = tempo.update_customer(customer_id, { + "name": "Updated Customer Name" + }) + + # Delete customer + tempo.delete_customer(customer_id) + +Holiday Management +------------------ +.. code-block:: python + + # Get all holidays + holidays = tempo.get_holidays() + + # Get specific holiday + holiday = tempo.get_holiday(holiday_id) + + # Create new holiday + new_holiday = tempo.create_holiday({ + "name": "Christmas Day", + "date": "2024-12-25", + "description": "Company holiday" + }) + + # Update holiday + updated_holiday = tempo.update_holiday(holiday_id, { + "name": "Updated Holiday Name" + }) + + # Delete holiday + tempo.delete_holiday(holiday_id) + +Report Generation +----------------- +.. code-block:: python + + # Generate report + report = tempo.generate_report("timesheet", { + "dateFrom": "2024-01-01", + "dateTo": "2024-01-31" + }) + + # Check report status + status = tempo.get_report_status(report_id) + + # Download report + report_data = tempo.download_report(report_id) + +Utility Methods +--------------- +.. code-block:: python + + # Get API metadata + metadata = tempo.get_metadata() + + # Check API health + health = tempo.get_health() + +Tempo Server +------------ + +The Tempo Server client provides access to various server-side API modules +for self-hosted Tempo instances. + +Basic Usage +----------- +Initialize the base Tempo Server client: + +.. code-block:: python + + tempo = TempoServer( + url="https://your-tempo-server.com", + token="your-tempo-api-token", + cloud=False + ) + +Specialized Client Classes +--------------------------- +For specific functionality, use the specialized client classes: + +Accounts API +------------ +.. code-block:: python + + from atlassian.tempo import TempoServerAccounts + + accounts_client = TempoServerAccounts( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all accounts + accounts = accounts_client.get_accounts() + + # Create new account + new_account = accounts_client.create_account({ + "name": "New Account", + "key": "NEW" + }) + +Teams API +--------- +.. code-block:: python + + from atlassian.tempo import TempoServerTeams + + teams_client = TempoServerTeams( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all teams + teams = teams_client.get_teams() + + # Create new team + new_team = teams_client.create_team({ + "name": "New Team", + "description": "Team description" + }) + + # Add member to team + teams_client.add_team_member(team_id, user_id) + +Planner API +----------- +.. code-block:: python + + from atlassian.tempo import TempoServerPlanner + + planner_client = TempoServerPlanner( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all plans + plans = planner_client.get_plans() + + # Create new plan + new_plan = planner_client.create_plan({ + "name": "New Plan", + "description": "Plan description" + }) + + # Get plan assignments + assignments = planner_client.get_plan_assignments(plan_id) + +Budgets API +----------- +.. code-block:: python + + from atlassian.tempo import TempoServerBudgets + + budgets_client = TempoServerBudgets( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all budgets + budgets = budgets_client.get_budgets() + + # Create new budget + new_budget = budgets_client.create_budget({ + "name": "New Budget", + "amount": 10000 + }) + + # Get budget allocations + allocations = budgets_client.get_budget_allocations(budget_id) + +Timesheets API +-------------- +.. code-block:: python + + from atlassian.tempo import TempoServerTimesheets + + timesheets_client = TempoServerTimesheets( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all timesheets + timesheets = timesheets_client.get_timesheets() + + # Create new timesheet + new_timesheet = timesheets_client.create_timesheet({ + "name": "New Timesheet", + "userId": 1 + }) + + # Submit timesheet for approval + timesheets_client.submit_timesheet(timesheet_id) + + # Approve timesheet + timesheets_client.approve_timesheet(timesheet_id) + + # Reject timesheet + timesheets_client.reject_timesheet(timesheet_id, "Invalid entries") + +Servlet API +----------- +.. code-block:: python + + from atlassian.tempo import TempoServerServlet + + servlet_client = TempoServerServlet( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all worklogs + worklogs = servlet_client.get_worklogs() + + # Create new worklog + new_worklog = servlet_client.create_worklog({ + "issueKey": "TEST-1", + "timeSpentSeconds": 3600 + }) + + # Get worklog attributes + attributes = servlet_client.get_worklog_attributes(worklog_id) + + # Update worklog attributes + servlet_client.update_worklog_attributes(worklog_id, { + "attribute1": "value1" + }) + +Events API +---------- +.. code-block:: python + + from atlassian.tempo import TempoServerEvents + + events_client = TempoServerEvents( + url="https://your-tempo-server.com", + token="your-tempo-api-token" + ) + + # Get all events + events = events_client.get_events() + + # Create new event + new_event = events_client.create_event({ + "type": "worklog_created", + "data": {"worklogId": 1} + }) + + # Get event subscriptions + subscriptions = events_client.get_event_subscriptions() + + # Create event subscription + new_subscription = events_client.create_event_subscription({ + "eventType": "worklog_created", + "url": "https://webhook.url" + }) + +API Configuration +----------------- +Both Cloud and Server clients support various configuration options: + +.. code-block:: python + + tempo = TempoCloud( + url="https://your-domain.atlassian.net", + token="your-tempo-api-token", + cloud=True, + timeout=75, + verify_ssl=True, + proxies={"http": "http://proxy:8080"}, + backoff_and_retry=True, + max_backoff_retries=1000 + ) + +Regional Endpoints +------------------ +For Tempo Cloud, you can use regional endpoints: + +- **Europe**: `https://api.eu.tempo.io` +- **Americas**: `https://api.us.tempo.io` +- **Global**: `https://api.tempo.io` + +.. code-block:: python + + # For European clients + tempo_eu = TempoCloud( + url="https://api.eu.tempo.io", + token="your-tempo-api-token" + ) + + # For American clients + tempo_us = TempoCloud( + url="https://api.us.tempo.io", + token="your-tempo-api-token" + ) + +Error Handling +-------------- +Both clients include proper error handling for common HTTP status codes: + +.. code-block:: python + + try: + accounts = tempo.get_accounts() + except Exception as e: + if "401" in str(e): + print("Authentication failed. Check your API token.") + elif "403" in str(e): + print("Access denied. Check your permissions.") + elif "404" in str(e): + print("Resource not found.") + elif "429" in str(e): + print("Rate limited. Wait before retrying.") + else: + print(f"Unexpected error: {e}") + +Rate Limiting +------------- +Both Tempo Cloud and Server APIs have rate limiting. The clients automatically +handle retries for rate-limited requests (status code 429). + +Examples +-------- + +See the `examples/tempo/` directory for complete working examples: + +- `tempo_cloud_example.py` - Cloud API usage +- `tempo_server_example.py` - Server API usage +- `tempo_integration_example.py` - Combined usage + +API Reference +------------- + +For detailed API documentation, visit: + +- **Tempo Cloud**: `Tempo Cloud API Documentation `_ +- **Tempo Server**: `Tempo Server API Documentation `_ + +Class Reference +--------------- + +.. autoclass:: atlassian.tempo.TempoCloud + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServer + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerAccounts + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerTeams + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerPlanner + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerBudgets + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerTimesheets + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerServlet + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: atlassian.tempo.TempoServerEvents + :members: + :undoc-members: + :show-inheritance: diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..e80f36666 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,21 @@ +[tool:pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers + --disable-warnings + --cov=atlassian + --cov-report=term-missing + --cov-report=html + --cov-report=xml +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests + unit: marks tests as unit tests +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..1a69ee31f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +# coding=utf-8 +""" +Pytest configuration and fixtures for Tempo tests. +""" + +import pytest +from unittest.mock import Mock, patch + +# Import mockup server for testing +from .mockup import mockup_server + + +@pytest.fixture(scope="session") +def mock_server_url(): + """Fixture providing the mock server URL.""" + return mockup_server() + + +@pytest.fixture +def mock_response(): + """Fixture providing a mock response object.""" + mock_resp = Mock() + mock_resp.status_code = 200 + mock_resp.json.return_value = {"success": True} + mock_resp.text = '{"success": true}' + mock_resp.content = b'{"success": true}' + return mock_resp + + +@pytest.fixture +def mock_session(): + """Fixture providing a mock session object.""" + with patch("requests.Session") as mock_session: + mock_session.return_value.request.return_value = Mock() + yield mock_session + + +@pytest.fixture +def tempo_cloud_client(): + """Fixture providing a TempoCloud client for testing.""" + from atlassian.tempo import TempoCloud + + return TempoCloud(url="https://test.atlassian.net", token="test-token", cloud=True) + + +@pytest.fixture +def tempo_server_client(): + """Fixture providing a TempoServer client for testing.""" + from atlassian.tempo import TempoServer + + return TempoServer(url="https://test.atlassian.net", token="test-token", cloud=False) diff --git a/tests/test_tempo_cloud.py b/tests/test_tempo_cloud.py new file mode 100644 index 000000000..d063d0356 --- /dev/null +++ b/tests/test_tempo_cloud.py @@ -0,0 +1,458 @@ +# coding=utf-8 +""" +Test cases for Tempo Cloud API client. +""" + +import pytest +from unittest.mock import patch + +from atlassian.tempo import TempoCloud + + +@pytest.fixture +def tempo_cloud(): + """Fixture for TempoCloud client.""" + return TempoCloud(url="https://test.atlassian.net", token="test-token", cloud=True) + + +class TestTempoCloud: + """Test cases for TempoCloud client.""" + + def test_init_defaults(self): + """Test TempoCloud client initialization with default values.""" + tempo = TempoCloud(url="https://test.atlassian.net", token="test-token") + assert tempo.api_version == "1" + assert tempo.api_root == "rest/tempo-timesheets/4" + + def test_init_custom_values(self): + """Test TempoCloud client initialization with custom values.""" + tempo = TempoCloud( + url="https://test.atlassian.net", token="test-token", api_version="2", api_root="custom/api/root" + ) + assert tempo.api_version == "2" + assert tempo.api_root == "custom/api/root" + + # Account Management Tests + @patch.object(TempoCloud, "get") + def test_get_accounts(self, mock_get, tempo_cloud): + """Test get_accounts method.""" + mock_get.return_value = [{"id": 1, "name": "Test Account"}] + result = tempo_cloud.get_accounts() + mock_get.assert_called_once_with("accounts", **{}) + assert result == [{"id": 1, "name": "Test Account"}] + + @patch.object(TempoCloud, "get") + def test_get_account(self, mock_get, tempo_cloud): + """Test get_account method.""" + mock_get.return_value = {"id": 1, "name": "Test Account"} + result = tempo_cloud.get_account(1) + mock_get.assert_called_once_with("accounts/1", **{}) + assert result == {"id": 1, "name": "Test Account"} + + @patch.object(TempoCloud, "post") + def test_create_account(self, mock_post, tempo_cloud): + """Test create_account method.""" + account_data = {"name": "New Account", "key": "NEW"} + mock_post.return_value = {"id": 2, "name": "New Account", "key": "NEW"} + result = tempo_cloud.create_account(account_data) + mock_post.assert_called_once_with("accounts", data=account_data, **{}) + assert result == {"id": 2, "name": "New Account", "key": "NEW"} + + @patch.object(TempoCloud, "put") + def test_update_account(self, mock_put, tempo_cloud): + """Test update_account method.""" + account_data = {"name": "Updated Account"} + mock_put.return_value = {"id": 1, "name": "Updated Account"} + result = tempo_cloud.update_account(1, account_data) + mock_put.assert_called_once_with("accounts/1", data=account_data, **{}) + assert result == {"id": 1, "name": "Updated Account"} + + @patch.object(TempoCloud, "delete") + def test_delete_account(self, mock_delete, tempo_cloud): + """Test delete_account method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_account(1) + mock_delete.assert_called_once_with("accounts/1", **{}) + assert result == {"success": True} + + # Worklog Management Tests + @patch.object(TempoCloud, "get") + def test_get_worklogs(self, mock_get, tempo_cloud): + """Test get_worklogs method.""" + mock_get.return_value = [{"id": 1, "timeSpentSeconds": 3600}] + result = tempo_cloud.get_worklogs() + mock_get.assert_called_once_with("worklogs", **{}) + assert result == [{"id": 1, "timeSpentSeconds": 3600}] + + @patch.object(TempoCloud, "get") + def test_get_worklog(self, mock_get, tempo_cloud): + """Test get_worklog method.""" + mock_get.return_value = {"id": 1, "timeSpentSeconds": 3600} + result = tempo_cloud.get_worklog(1) + mock_get.assert_called_once_with("worklogs/1", **{}) + assert result == {"id": 1, "timeSpentSeconds": 3600} + + @patch.object(TempoCloud, "post") + def test_create_worklog(self, mock_post, tempo_cloud): + """Test create_worklog method.""" + worklog_data = {"issueKey": "TEST-1", "timeSpentSeconds": 3600} + mock_post.return_value = {"id": 2, "issueKey": "TEST-1", "timeSpentSeconds": 3600} + result = tempo_cloud.create_worklog(worklog_data) + mock_post.assert_called_once_with("worklogs", data=worklog_data, **{}) + assert result == {"id": 2, "issueKey": "TEST-1", "timeSpentSeconds": 3600} + + @patch.object(TempoCloud, "put") + def test_update_worklog(self, mock_put, tempo_cloud): + """Test update_worklog method.""" + worklog_data = {"timeSpentSeconds": 7200} + mock_put.return_value = {"id": 1, "timeSpentSeconds": 7200} + result = tempo_cloud.update_worklog(1, worklog_data) + mock_put.assert_called_once_with("worklogs/1", data=worklog_data, **{}) + assert result == {"id": 1, "timeSpentSeconds": 7200} + + @patch.object(TempoCloud, "delete") + def test_delete_worklog(self, mock_delete, tempo_cloud): + """Test delete_worklog method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_worklog(1) + mock_delete.assert_called_once_with("worklogs/1", **{}) + assert result == {"success": True} + + # Schedule Management Tests + @patch.object(TempoCloud, "get") + def test_get_schedules(self, mock_get, tempo_cloud): + """Test get_schedules method.""" + mock_get.return_value = [{"id": 1, "name": "Test Schedule"}] + result = tempo_cloud.get_schedules() + mock_get.assert_called_once_with("schedules", **{}) + assert result == [{"id": 1, "name": "Test Schedule"}] + + @patch.object(TempoCloud, "get") + def test_get_schedule(self, mock_get, tempo_cloud): + """Test get_schedule method.""" + mock_get.return_value = {"id": 1, "name": "Test Schedule"} + result = tempo_cloud.get_schedule(1) + mock_get.assert_called_once_with("schedules/1", **{}) + assert result == {"id": 1, "name": "Test Schedule"} + + @patch.object(TempoCloud, "post") + def test_create_schedule(self, mock_post, tempo_cloud): + """Test create_schedule method.""" + schedule_data = {"name": "New Schedule", "userId": 1} + mock_post.return_value = {"id": 2, "name": "New Schedule", "userId": 1} + result = tempo_cloud.create_schedule(schedule_data) + mock_post.assert_called_once_with("schedules", data=schedule_data, **{}) + assert result == {"id": 2, "name": "New Schedule", "userId": 1} + + @patch.object(TempoCloud, "put") + def test_update_schedule(self, mock_put, tempo_cloud): + """Test update_schedule method.""" + schedule_data = {"name": "Updated Schedule"} + mock_put.return_value = {"id": 1, "name": "Updated Schedule"} + result = tempo_cloud.update_schedule(1, schedule_data) + mock_put.assert_called_once_with("schedules/1", data=schedule_data, **{}) + assert result == {"id": 1, "name": "Updated Schedule"} + + @patch.object(TempoCloud, "delete") + def test_delete_schedule(self, mock_delete, tempo_cloud): + """Test delete_schedule method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_schedule(1) + mock_delete.assert_called_once_with("schedules/1", **{}) + assert result == {"success": True} + + # User Management Tests + @patch.object(TempoCloud, "get") + def test_get_users(self, mock_get, tempo_cloud): + """Test get_users method.""" + mock_get.return_value = [{"id": 1, "name": "Test User"}] + result = tempo_cloud.get_users() + mock_get.assert_called_once_with("users", **{}) + assert result == [{"id": 1, "name": "Test User"}] + + @patch.object(TempoCloud, "get") + def test_get_user(self, mock_get, tempo_cloud): + """Test get_user method.""" + mock_get.return_value = {"id": 1, "name": "Test User"} + result = tempo_cloud.get_user(1) + mock_get.assert_called_once_with("users/1", **{}) + assert result == {"id": 1, "name": "Test User"} + + @patch.object(TempoCloud, "get") + def test_get_user_schedule(self, mock_get, tempo_cloud): + """Test get_user_schedule method.""" + mock_get.return_value = {"id": 1, "userId": 1} + result = tempo_cloud.get_user_schedule(1) + mock_get.assert_called_once_with("users/1/schedule", **{}) + assert result == {"id": 1, "userId": 1} + + @patch.object(TempoCloud, "get") + def test_get_user_worklogs(self, mock_get, tempo_cloud): + """Test get_user_worklogs method.""" + mock_get.return_value = [{"id": 1, "userId": 1}] + result = tempo_cloud.get_user_worklogs(1) + mock_get.assert_called_once_with("users/1/worklogs", **{}) + assert result == [{"id": 1, "userId": 1}] + + # Team Management Tests + @patch.object(TempoCloud, "get") + def test_get_teams(self, mock_get, tempo_cloud): + """Test get_teams method.""" + mock_get.return_value = [{"id": 1, "name": "Test Team"}] + result = tempo_cloud.get_teams() + mock_get.assert_called_once_with("teams", **{}) + assert result == [{"id": 1, "name": "Test Team"}] + + @patch.object(TempoCloud, "get") + def test_get_team(self, mock_get, tempo_cloud): + """Test get_team method.""" + mock_get.return_value = {"id": 1, "name": "Test Team"} + result = tempo_cloud.get_team(1) + mock_get.assert_called_once_with("teams/1", **{}) + assert result == {"id": 1, "name": "Test Team"} + + @patch.object(TempoCloud, "post") + def test_create_team(self, mock_post, tempo_cloud): + """Test create_team method.""" + team_data = {"name": "New Team"} + mock_post.return_value = {"id": 2, "name": "New Team"} + result = tempo_cloud.create_team(team_data) + mock_post.assert_called_once_with("teams", data=team_data, **{}) + assert result == {"id": 2, "name": "New Team"} + + @patch.object(TempoCloud, "put") + def test_update_team(self, mock_put, tempo_cloud): + """Test update_team method.""" + team_data = {"name": "Updated Team"} + mock_put.return_value = {"id": 1, "name": "Updated Team"} + result = tempo_cloud.update_team(1, team_data) + mock_put.assert_called_once_with("teams/1", data=team_data, **{}) + assert result == {"id": 1, "name": "Updated Team"} + + @patch.object(TempoCloud, "delete") + def test_delete_team(self, mock_delete, tempo_cloud): + """Test delete_team method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_team(1) + mock_delete.assert_called_once_with("teams/1", **{}) + assert result == {"success": True} + + @patch.object(TempoCloud, "get") + def test_get_team_members(self, mock_get, tempo_cloud): + """Test get_team_members method.""" + mock_get.return_value = [{"id": 1, "name": "Member 1"}] + result = tempo_cloud.get_team_members(1) + mock_get.assert_called_once_with("teams/1/members", **{}) + assert result == [{"id": 1, "name": "Member 1"}] + + @patch.object(TempoCloud, "post") + def test_add_team_member(self, mock_post, tempo_cloud): + """Test add_team_member method.""" + mock_post.return_value = {"success": True} + result = tempo_cloud.add_team_member(1, 2) + mock_post.assert_called_once_with("teams/1/members", data={"userId": 2}, **{}) + assert result == {"success": True} + + @patch.object(TempoCloud, "delete") + def test_remove_team_member(self, mock_delete, tempo_cloud): + """Test remove_team_member method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.remove_team_member(1, 2) + mock_delete.assert_called_once_with("teams/1/members/2", **{}) + assert result == {"success": True} + + # Project Management Tests + @patch.object(TempoCloud, "get") + def test_get_projects(self, mock_get, tempo_cloud): + """Test get_projects method.""" + mock_get.return_value = [{"id": 1, "name": "Test Project"}] + result = tempo_cloud.get_projects() + mock_get.assert_called_once_with("projects", **{}) + assert result == [{"id": 1, "name": "Test Project"}] + + @patch.object(TempoCloud, "get") + def test_get_project(self, mock_get, tempo_cloud): + """Test get_project method.""" + mock_get.return_value = {"id": 1, "name": "Test Project"} + result = tempo_cloud.get_project(1) + mock_get.assert_called_once_with("projects/1", **{}) + assert result == {"id": 1, "name": "Test Project"} + + @patch.object(TempoCloud, "get") + def test_get_project_worklogs(self, mock_get, tempo_cloud): + """Test get_project_worklogs method.""" + mock_get.return_value = [{"id": 1, "projectId": 1}] + result = tempo_cloud.get_project_worklogs(1) + mock_get.assert_called_once_with("projects/1/worklogs", **{}) + assert result == [{"id": 1, "projectId": 1}] + + # Activity Management Tests + @patch.object(TempoCloud, "get") + def test_get_activities(self, mock_get, tempo_cloud): + """Test get_activities method.""" + mock_get.return_value = [{"id": 1, "name": "Test Activity"}] + result = tempo_cloud.get_activities() + mock_get.assert_called_once_with("activities", **{}) + assert result == [{"id": 1, "name": "Test Activity"}] + + @patch.object(TempoCloud, "get") + def test_get_activity(self, mock_get, tempo_cloud): + """Test get_activity method.""" + mock_get.return_value = {"id": 1, "name": "Test Activity"} + result = tempo_cloud.get_activity(1) + mock_get.assert_called_once_with("activities/1", **{}) + assert result == {"id": 1, "name": "Test Activity"} + + @patch.object(TempoCloud, "post") + def test_create_activity(self, mock_post, tempo_cloud): + """Test create_activity method.""" + activity_data = {"name": "New Activity"} + mock_post.return_value = {"id": 2, "name": "New Activity"} + result = tempo_cloud.create_activity(activity_data) + mock_post.assert_called_once_with("activities", data=activity_data, **{}) + assert result == {"id": 2, "name": "New Activity"} + + @patch.object(TempoCloud, "put") + def test_update_activity(self, mock_put, tempo_cloud): + """Test update_activity method.""" + activity_data = {"name": "Updated Activity"} + mock_put.return_value = {"id": 1, "name": "Updated Activity"} + result = tempo_cloud.update_activity(1, activity_data) + mock_put.assert_called_once_with("activities/1", data=activity_data, **{}) + assert result == {"id": 1, "name": "Updated Activity"} + + @patch.object(TempoCloud, "delete") + def test_delete_activity(self, mock_delete, tempo_cloud): + """Test delete_activity method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_activity(1) + mock_delete.assert_called_once_with("activities/1", **{}) + assert result == {"success": True} + + # Customer Management Tests + @patch.object(TempoCloud, "get") + def test_get_customers(self, mock_get, tempo_cloud): + """Test get_customers method.""" + mock_get.return_value = [{"id": 1, "name": "Test Customer"}] + result = tempo_cloud.get_customers() + mock_get.assert_called_once_with("customers", **{}) + assert result == [{"id": 1, "name": "Test Customer"}] + + @patch.object(TempoCloud, "get") + def test_get_customer(self, mock_get, tempo_cloud): + """Test get_customer method.""" + mock_get.return_value = {"id": 1, "name": "Test Customer"} + result = tempo_cloud.get_customer(1) + mock_get.assert_called_once_with("customers/1", **{}) + assert result == {"id": 1, "name": "Test Customer"} + + @patch.object(TempoCloud, "post") + def test_create_customer(self, mock_post, tempo_cloud): + """Test create_customer method.""" + customer_data = {"name": "New Customer"} + mock_post.return_value = {"id": 2, "name": "New Customer"} + result = tempo_cloud.create_customer(customer_data) + mock_post.assert_called_once_with("customers", data=customer_data, **{}) + assert result == {"id": 2, "name": "New Customer"} + + @patch.object(TempoCloud, "put") + def test_update_customer(self, mock_put, tempo_cloud): + """Test update_customer method.""" + customer_data = {"name": "Updated Customer"} + mock_put.return_value = {"id": 1, "name": "Updated Customer"} + result = tempo_cloud.update_customer(1, customer_data) + mock_put.assert_called_once_with("customers/1", data=customer_data, **{}) + assert result == {"id": 1, "name": "Updated Customer"} + + @patch.object(TempoCloud, "delete") + def test_delete_customer(self, mock_delete, tempo_cloud): + """Test delete_customer method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_customer(1) + mock_delete.assert_called_once_with("customers/1", **{}) + assert result == {"success": True} + + # Holiday Management Tests + @patch.object(TempoCloud, "get") + def test_get_holidays(self, mock_get, tempo_cloud): + """Test get_holidays method.""" + mock_get.return_value = [{"id": 1, "name": "Test Holiday"}] + result = tempo_cloud.get_holidays() + mock_get.assert_called_once_with("holidays", **{}) + assert result == [{"id": 1, "name": "Test Holiday"}] + + @patch.object(TempoCloud, "get") + def test_get_holiday(self, mock_get, tempo_cloud): + """Test get_holiday method.""" + mock_get.return_value = {"id": 1, "name": "Test Holiday"} + result = tempo_cloud.get_holiday(1) + mock_get.assert_called_once_with("holidays/1", **{}) + assert result == {"id": 1, "name": "Test Holiday"} + + @patch.object(TempoCloud, "post") + def test_create_holiday(self, mock_post, tempo_cloud): + """Test create_holiday method.""" + holiday_data = {"name": "New Holiday", "date": "2024-01-01"} + mock_post.return_value = {"id": 2, "name": "New Holiday", "date": "2024-01-01"} + result = tempo_cloud.create_holiday(holiday_data) + mock_post.assert_called_once_with("holidays", data=holiday_data, **{}) + assert result == {"id": 2, "name": "New Holiday", "date": "2024-01-01"} + + @patch.object(TempoCloud, "put") + def test_update_holiday(self, mock_put, tempo_cloud): + """Test update_holiday method.""" + holiday_data = {"name": "Updated Holiday"} + mock_put.return_value = {"id": 1, "name": "Updated Holiday"} + result = tempo_cloud.update_holiday(1, holiday_data) + mock_put.assert_called_once_with("holidays/1", data=holiday_data, **{}) + assert result == {"id": 1, "name": "Updated Holiday"} + + @patch.object(TempoCloud, "delete") + def test_delete_holiday(self, mock_delete, tempo_cloud): + """Test delete_holiday method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_holiday(1) + mock_delete.assert_called_once_with("holidays/1", **{}) + assert result == {"success": True} + + # Report Generation Tests + @patch.object(TempoCloud, "post") + def test_generate_report(self, mock_post, tempo_cloud): + """Test generate_report method.""" + mock_post.return_value = {"reportId": "123"} + result = tempo_cloud.generate_report("timesheet", {"dateFrom": "2024-01-01"}) + mock_post.assert_called_once_with("reports/timesheet", data={"dateFrom": "2024-01-01"}, **{}) + assert result == {"reportId": "123"} + + @patch.object(TempoCloud, "get") + def test_get_report_status(self, mock_get, tempo_cloud): + """Test get_report_status method.""" + mock_get.return_value = {"status": "completed"} + result = tempo_cloud.get_report_status("123") + mock_get.assert_called_once_with("reports/123/status", **{}) + assert result == {"status": "completed"} + + @patch.object(TempoCloud, "get") + def test_download_report(self, mock_get, tempo_cloud): + """Test download_report method.""" + mock_get.return_value = {"content": "report data"} + result = tempo_cloud.download_report("123") + mock_get.assert_called_once_with("reports/123/download", **{}) + assert result == {"content": "report data"} + + # Utility Methods Tests + @patch.object(TempoCloud, "get") + def test_get_metadata(self, mock_get, tempo_cloud): + """Test get_metadata method.""" + mock_get.return_value = {"version": "1.0.0"} + result = tempo_cloud.get_metadata() + mock_get.assert_called_once_with("metadata", **{}) + assert result == {"version": "1.0.0"} + + @patch.object(TempoCloud, "get") + def test_get_health(self, mock_get, tempo_cloud): + """Test get_health method.""" + mock_get.return_value = {"status": "healthy"} + result = tempo_cloud.get_health() + mock_get.assert_called_once_with("health", **{}) + assert result == {"status": "healthy"} diff --git a/tests/test_tempo_cloud_pytest.py b/tests/test_tempo_cloud_pytest.py new file mode 100644 index 000000000..35ebe8c47 --- /dev/null +++ b/tests/test_tempo_cloud_pytest.py @@ -0,0 +1,195 @@ +# coding=utf-8 +""" +Test cases for Tempo Cloud API client using pytest. +""" + +import pytest +from unittest.mock import patch + +from atlassian.tempo import TempoCloud + + +@pytest.fixture +def tempo_cloud(): + """Fixture for TempoCloud client.""" + return TempoCloud(url="https://test.atlassian.net", token="test-token", cloud=True) + + +class TestTempoCloud: + """Test cases for TempoCloud client.""" + + def test_init_defaults(self): + """Test TempoCloud client initialization with default values.""" + tempo = TempoCloud(url="https://test.atlassian.net", token="test-token") + assert tempo.api_version == "1" + assert tempo.api_root == "rest/tempo-timesheets/4" + + def test_init_custom_values(self): + """Test TempoCloud client initialization with custom values.""" + tempo = TempoCloud( + url="https://test.atlassian.net", token="test-token", api_version="2", api_root="custom/api/root" + ) + assert tempo.api_version == "2" + assert tempo.api_root == "custom/api/root" + + # Account Management Tests + @patch.object(TempoCloud, "get") + def test_get_accounts(self, mock_get, tempo_cloud): + """Test get_accounts method.""" + mock_get.return_value = [{"id": 1, "name": "Test Account"}] + result = tempo_cloud.get_accounts() + mock_get.assert_called_once_with("accounts", **{}) + assert result == [{"id": 1, "name": "Test Account"}] + + @patch.object(TempoCloud, "get") + def test_get_account(self, mock_get, tempo_cloud): + """Test get_account method.""" + mock_get.return_value = {"id": 1, "name": "Test Account"} + result = tempo_cloud.get_account(1) + mock_get.assert_called_once_with("accounts/1", **{}) + assert result == {"id": 1, "name": "Test Account"} + + @patch.object(TempoCloud, "post") + def test_create_account(self, mock_post, tempo_cloud): + """Test create_account method.""" + account_data = {"name": "New Account", "key": "NEW"} + mock_post.return_value = {"id": 2, "name": "New Account", "key": "NEW"} + result = tempo_cloud.create_account(account_data) + mock_post.assert_called_once_with("accounts", data=account_data, **{}) + assert result == {"id": 2, "name": "New Account", "key": "NEW"} + + @patch.object(TempoCloud, "put") + def test_update_account(self, mock_put, tempo_cloud): + """Test update_account method.""" + account_data = {"name": "Updated Account"} + mock_put.return_value = {"id": 1, "name": "Updated Account"} + result = tempo_cloud.update_account(1, account_data) + mock_put.assert_called_once_with("accounts/1", data=account_data, **{}) + assert result == {"id": 1, "name": "Updated Account"} + + @patch.object(TempoCloud, "delete") + def test_delete_account(self, mock_delete, tempo_cloud): + """Test delete_account method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_account(1) + mock_delete.assert_called_once_with("accounts/1", **{}) + assert result == {"success": True} + + # Worklog Management Tests + @patch.object(TempoCloud, "get") + def test_get_worklogs(self, mock_get, tempo_cloud): + """Test get_worklogs method.""" + mock_get.return_value = [{"id": 1, "timeSpentSeconds": 3600}] + result = tempo_cloud.get_worklogs() + mock_get.assert_called_once_with("worklogs", **{}) + assert result == [{"id": 1, "timeSpentSeconds": 3600}] + + @patch.object(TempoCloud, "get") + def test_get_worklog(self, mock_get, tempo_cloud): + """Test get_worklog method.""" + mock_get.return_value = {"id": 1, "timeSpentSeconds": 3600} + result = tempo_cloud.get_worklog(1) + mock_get.assert_called_once_with("worklogs/1", **{}) + assert result == {"id": 1, "timeSpentSeconds": 3600} + + @patch.object(TempoCloud, "post") + def test_create_worklog(self, mock_post, tempo_cloud): + """Test create_worklog method.""" + worklog_data = {"issueKey": "TEST-1", "timeSpentSeconds": 3600} + mock_post.return_value = {"id": 2, "issueKey": "TEST-1", "timeSpentSeconds": 3600} + result = tempo_cloud.create_worklog(worklog_data) + mock_post.assert_called_once_with("worklogs", data=worklog_data, **{}) + assert result == {"id": 2, "issueKey": "TEST-1", "timeSpentSeconds": 3600} + + @patch.object(TempoCloud, "put") + def test_update_worklog(self, mock_put, tempo_cloud): + """Test update_worklog method.""" + worklog_data = {"timeSpentSeconds": 7200} + mock_put.return_value = {"id": 1, "timeSpentSeconds": 7200} + result = tempo_cloud.update_worklog(1, worklog_data) + mock_put.assert_called_once_with("worklogs/1", data=worklog_data, **{}) + assert result == {"id": 1, "timeSpentSeconds": 7200} + + @patch.object(TempoCloud, "delete") + def test_delete_worklog(self, mock_delete, tempo_cloud): + """Test delete_worklog method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_worklog(1) + mock_delete.assert_called_once_with("worklogs/1", **{}) + assert result == {"success": True} + + # Team Management Tests + @patch.object(TempoCloud, "get") + def test_get_teams(self, mock_get, tempo_cloud): + """Test get_teams method.""" + mock_get.return_value = [{"id": 1, "name": "Test Team"}] + result = tempo_cloud.get_teams() + mock_get.assert_called_once_with("teams", **{}) + assert result == [{"id": 1, "name": "Test Team"}] + + @patch.object(TempoCloud, "post") + def test_create_team(self, mock_post, tempo_cloud): + """Test create_team method.""" + team_data = {"name": "New Team"} + mock_post.return_value = {"id": 2, "name": "New Team"} + result = tempo_cloud.create_team(team_data) + mock_post.assert_called_once_with("teams", data=team_data, **{}) + assert result == {"id": 2, "name": "New Team"} + + @patch.object(TempoCloud, "put") + def test_update_team(self, mock_put, tempo_cloud): + """Test update_team method.""" + team_data = {"name": "Updated Team"} + mock_put.return_value = {"id": 1, "name": "Updated Team"} + result = tempo_cloud.update_team(1, team_data) + mock_put.assert_called_once_with("teams/1", data=team_data, **{}) + assert result == {"id": 1, "name": "Updated Team"} + + @patch.object(TempoCloud, "delete") + def test_delete_team(self, mock_delete, tempo_cloud): + """Test delete_team method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.delete_team(1) + mock_delete.assert_called_once_with("teams/1", **{}) + assert result == {"success": True} + + @patch.object(TempoCloud, "get") + def test_get_team_members(self, mock_get, tempo_cloud): + """Test get_team_members method.""" + mock_get.return_value = [{"id": 1, "name": "Member 1"}] + result = tempo_cloud.get_team_members(1) + mock_get.assert_called_once_with("teams/1/members", **{}) + assert result == [{"id": 1, "name": "Member 1"}] + + @patch.object(TempoCloud, "post") + def test_add_team_member(self, mock_post, tempo_cloud): + """Test add_team_member method.""" + mock_post.return_value = {"success": True} + result = tempo_cloud.add_team_member(1, 2) + mock_post.assert_called_once_with("teams/1/members", data={"userId": 2}, **{}) + assert result == {"success": True} + + @patch.object(TempoCloud, "delete") + def test_remove_team_member(self, mock_delete, tempo_cloud): + """Test remove_team_member method.""" + mock_delete.return_value = {"success": True} + result = tempo_cloud.remove_team_member(1, 2) + mock_delete.assert_called_once_with("teams/1/members/2", **{}) + assert result == {"success": True} + + # Utility Methods Tests + @patch.object(TempoCloud, "get") + def test_get_metadata(self, mock_get, tempo_cloud): + """Test get_metadata method.""" + mock_get.return_value = {"version": "1.0.0"} + result = tempo_cloud.get_metadata() + mock_get.assert_called_once_with("metadata", **{}) + assert result == {"version": "1.0.0"} + + @patch.object(TempoCloud, "get") + def test_get_health(self, mock_get, tempo_cloud): + """Test get_health method.""" + mock_get.return_value = {"status": "healthy"} + result = tempo_cloud.get_health() + mock_get.assert_called_once_with("health", **{}) + assert result == {"status": "healthy"} diff --git a/tests/test_tempo_server.py b/tests/test_tempo_server.py new file mode 100644 index 000000000..f52eed24f --- /dev/null +++ b/tests/test_tempo_server.py @@ -0,0 +1,229 @@ +# coding=utf-8 +""" +Test cases for Tempo Server API clients. +""" + +import unittest +from unittest.mock import patch + +from atlassian.tempo import TempoServer + + +class TestTempoServer(unittest.TestCase): + """Test cases for base TempoServer client.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token", cloud=False) + + def test_init_defaults(self): + """Test TempoServer client initialization with default values.""" + tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + self.assertEqual(tempo.api_version, "1") + self.assertEqual(tempo.api_root, "rest/tempo-core/1") + + def test_init_custom_values(self): + """Test TempoServer client initialization with custom values.""" + tempo = TempoServer( + url="https://test.atlassian.net", token="test-token", api_version="2", api_root="custom/api/root" + ) + self.assertEqual(tempo.api_version, "2") + self.assertEqual(tempo.api_root, "custom/api/root") + + def test_specialized_modules_exist(self): + """Test that specialized modules are properly initialized.""" + self.assertIsNotNone(self.tempo.accounts) + self.assertIsNotNone(self.tempo.teams) + self.assertIsNotNone(self.tempo.planner) + self.assertIsNotNone(self.tempo.budgets) + self.assertIsNotNone(self.tempo.timesheets) + self.assertIsNotNone(self.tempo.servlet) + self.assertIsNotNone(self.tempo.events) + + @patch.object(TempoServer, "get") + def test_get_health(self, mock_get): + """Test get_health method.""" + mock_get.return_value = {"status": "healthy"} + result = self.tempo.get_health() + mock_get.assert_called_once_with("health", **{}) + self.assertEqual(result, {"status": "healthy"}) + + @patch.object(TempoServer, "get") + def test_get_metadata(self, mock_get): + """Test get_metadata method.""" + mock_get.return_value = {"version": "1.0.0"} + result = self.tempo.get_metadata() + mock_get.assert_called_once_with("metadata", **{}) + self.assertEqual(result, {"version": "1.0.0"}) + + +class TestTempoServerAccounts(unittest.TestCase): + """Test cases for TempoServer accounts module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_accounts(self, mock_get): + """Test get_accounts method.""" + mock_get.return_value = [{"id": 1, "name": "Test Account"}] + result = self.tempo.accounts.get_accounts() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "name": "Test Account"}]) + + @patch.object(TempoServer, "get") + def test_get_account(self, mock_get): + """Test get_account method.""" + mock_get.return_value = {"id": 1, "name": "Test Account"} + result = self.tempo.accounts.get_account(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "name": "Test Account"}) + + +class TestTempoServerTeams(unittest.TestCase): + """Test cases for TempoServer teams module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_teams(self, mock_get): + """Test get_teams method.""" + mock_get.return_value = [{"id": 1, "name": "Test Team"}] + result = self.tempo.teams.get_teams() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "name": "Test Team"}]) + + @patch.object(TempoServer, "get") + def test_get_team(self, mock_get): + """Test get_team method.""" + mock_get.return_value = {"id": 1, "name": "Test Team"} + result = self.tempo.teams.get_team(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "name": "Test Team"}) + + +class TestTempoServerPlanner(unittest.TestCase): + """Test cases for TempoServer planner module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_plans(self, mock_get): + """Test get_plans method.""" + mock_get.return_value = [{"id": 1, "name": "Test Plan"}] + result = self.tempo.planner.get_plans() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "name": "Test Plan"}]) + + @patch.object(TempoServer, "get") + def test_get_plan(self, mock_get): + """Test get_plan method.""" + mock_get.return_value = {"id": 1, "name": "Test Plan"} + result = self.tempo.planner.get_plan(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "name": "Test Plan"}) + + +class TestTempoServerBudgets(unittest.TestCase): + """Test cases for TempoServer budgets module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_budgets(self, mock_get): + """Test get_budgets method.""" + mock_get.return_value = [{"id": 1, "name": "Test Budget"}] + result = self.tempo.budgets.get_budgets() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "name": "Test Budget"}]) + + @patch.object(TempoServer, "get") + def test_get_budget(self, mock_get): + """Test get_budget method.""" + mock_get.return_value = {"id": 1, "name": "Test Budget"} + result = self.tempo.budgets.get_budget(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "name": "Test Budget"}) + + +class TestTempoServerTimesheets(unittest.TestCase): + """Test cases for TempoServer timesheets module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_timesheets(self, mock_get): + """Test get_timesheets method.""" + mock_get.return_value = [{"id": 1, "name": "Test Timesheet"}] + result = self.tempo.timesheets.get_timesheets() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "name": "Test Timesheet"}]) + + @patch.object(TempoServer, "get") + def test_get_timesheet(self, mock_get): + """Test get_timesheet method.""" + mock_get.return_value = {"id": 1, "name": "Test Timesheet"} + result = self.tempo.timesheets.get_timesheet(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "name": "Test Timesheet"}) + + +class TestTempoServerServlet(unittest.TestCase): + """Test cases for TempoServer servlet module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_worklogs(self, mock_get): + """Test get_worklogs method.""" + mock_get.return_value = [{"id": 1, "timeSpentSeconds": 3600}] + result = self.tempo.servlet.get_worklogs() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "timeSpentSeconds": 3600}]) + + @patch.object(TempoServer, "get") + def test_get_worklog(self, mock_get): + """Test get_worklog method.""" + mock_get.return_value = {"id": 1, "timeSpentSeconds": 3600} + result = self.tempo.servlet.get_worklog(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "timeSpentSeconds": 3600}) + + +class TestTempoServerEvents(unittest.TestCase): + """Test cases for TempoServer events module.""" + + def setUp(self): + """Set up test fixtures.""" + self.tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_events(self, mock_get): + """Test get_events method.""" + mock_get.return_value = [{"id": 1, "type": "worklog_created"}] + result = self.tempo.events.get_events() + mock_get.assert_called_once_with("", **{}) + self.assertEqual(result, [{"id": 1, "type": "worklog_created"}]) + + @patch.object(TempoServer, "get") + def test_get_event(self, mock_get): + """Test get_event method.""" + mock_get.return_value = {"id": 1, "type": "worklog_created"} + result = self.tempo.events.get_event(1) + mock_get.assert_called_once_with("1", **{}) + self.assertEqual(result, {"id": 1, "type": "worklog_created"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tempo_server_pytest.py b/tests/test_tempo_server_pytest.py new file mode 100644 index 000000000..a861b6361 --- /dev/null +++ b/tests/test_tempo_server_pytest.py @@ -0,0 +1,234 @@ +# coding=utf-8 +""" +Test cases for Tempo Server API clients using pytest. +""" + +import pytest +from unittest.mock import patch + +from atlassian.tempo import TempoServer + + +@pytest.fixture +def tempo_server(): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token", cloud=False) + + +class TestTempoServer: + """Test cases for base TempoServer client.""" + + def test_init_defaults(self): + """Test TempoServer client initialization with default values.""" + tempo = TempoServer(url="https://test.atlassian.net", token="test-token") + assert tempo.api_version == "1" + assert tempo.api_root == "rest/tempo-core/1" + + def test_init_custom_values(self): + """Test TempoServer client initialization with custom values.""" + tempo = TempoServer( + url="https://test.atlassian.net", token="test-token", api_version="2", api_root="custom/api/root" + ) + assert tempo.api_version == "2" + assert tempo.api_root == "custom/api/root" + + def test_specialized_modules_exist(self, tempo_server): + """Test that specialized modules are properly initialized.""" + assert tempo_server.accounts is not None + assert tempo_server.teams is not None + assert tempo_server.planner is not None + assert tempo_server.budgets is not None + assert tempo_server.timesheets is not None + assert tempo_server.servlet is not None + assert tempo_server.events is not None + + @patch.object(TempoServer, "get") + def test_get_health(self, mock_get, tempo_server): + """Test get_health method.""" + mock_get.return_value = {"status": "healthy"} + result = tempo_server.get_health() + mock_get.assert_called_once_with("health", **{}) + assert result == {"status": "healthy"} + + @patch.object(TempoServer, "get") + def test_get_metadata(self, mock_get, tempo_server): + """Test get_metadata method.""" + mock_get.return_value = {"version": "1.0.0"} + result = tempo_server.get_metadata() + mock_get.assert_called_once_with("metadata", **{}) + assert result == {"version": "1.0.0"} + + +class TestTempoServerAccounts: + """Test cases for TempoServer accounts module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_accounts(self, mock_get, tempo_server): + """Test get_accounts method.""" + mock_get.return_value = [{"id": 1, "name": "Test Account"}] + result = tempo_server.accounts.get_accounts() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "name": "Test Account"}] + + @patch.object(TempoServer, "get") + def test_get_account(self, mock_get, tempo_server): + """Test get_account method.""" + mock_get.return_value = {"id": 1, "name": "Test Account"} + result = tempo_server.accounts.get_account(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "name": "Test Account"} + + +class TestTempoServerTeams: + """Test cases for TempoServer teams module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_teams(self, mock_get, tempo_server): + """Test get_teams method.""" + mock_get.return_value = [{"id": 1, "name": "Test Team"}] + result = tempo_server.teams.get_teams() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "name": "Test Team"}] + + @patch.object(TempoServer, "get") + def test_get_team(self, mock_get, tempo_server): + """Test get_team method.""" + mock_get.return_value = {"id": 1, "name": "Test Team"} + result = tempo_server.teams.get_team(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "name": "Test Team"} + + +class TestTempoServerPlanner: + """Test cases for TempoServer planner module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_plans(self, mock_get, tempo_server): + """Test get_plans method.""" + mock_get.return_value = [{"id": 1, "name": "Test Plan"}] + result = tempo_server.planner.get_plans() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "name": "Test Plan"}] + + @patch.object(TempoServer, "get") + def test_get_plan(self, mock_get, tempo_server): + """Test get_plan method.""" + mock_get.return_value = {"id": 1, "name": "Test Plan"} + result = tempo_server.planner.get_plan(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "name": "Test Plan"} + + +class TestTempoServerBudgets: + """Test cases for TempoServer budgets module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_budgets(self, mock_get, tempo_server): + """Test get_budgets method.""" + mock_get.return_value = [{"id": 1, "name": "Test Budget"}] + result = tempo_server.budgets.get_budgets() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "name": "Test Budget"}] + + @patch.object(TempoServer, "get") + def test_get_budget(self, mock_get, tempo_server): + """Test get_budget method.""" + mock_get.return_value = {"id": 1, "name": "Test Budget"} + result = tempo_server.budgets.get_budget(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "name": "Test Budget"} + + +class TestTempoServerTimesheets: + """Test cases for TempoServer timesheets module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_timesheets(self, mock_get, tempo_server): + """Test get_timesheets method.""" + mock_get.return_value = [{"id": 1, "name": "Test Timesheet"}] + result = tempo_server.timesheets.get_timesheets() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "name": "Test Timesheet"}] + + @patch.object(TempoServer, "get") + def test_get_timesheet(self, mock_get, tempo_server): + """Test get_timesheet method.""" + mock_get.return_value = {"id": 1, "name": "Test Timesheet"} + result = tempo_server.timesheets.get_timesheet(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "name": "Test Timesheet"} + + +class TestTempoServerServlet: + """Test cases for TempoServer servlet module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_worklogs(self, mock_get, tempo_server): + """Test get_worklogs method.""" + mock_get.return_value = [{"id": 1, "timeSpentSeconds": 3600}] + result = tempo_server.servlet.get_worklogs() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "timeSpentSeconds": 3600}] + + @patch.object(TempoServer, "get") + def test_get_worklog(self, mock_get, tempo_server): + """Test get_worklog method.""" + mock_get.return_value = {"id": 1, "timeSpentSeconds": 3600} + result = tempo_server.servlet.get_worklog(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "timeSpentSeconds": 3600} + + +class TestTempoServerEvents: + """Test cases for TempoServer events module.""" + + @pytest.fixture + def tempo_server(self): + """Fixture for TempoServer client.""" + return TempoServer(url="https://test.atlassian.net", token="test-token") + + @patch.object(TempoServer, "get") + def test_get_events(self, mock_get, tempo_server): + """Test get_events method.""" + mock_get.return_value = [{"id": 1, "type": "worklog_created"}] + result = tempo_server.events.get_events() + mock_get.assert_called_once_with("", **{}) + assert result == [{"id": 1, "type": "worklog_created"}] + + @patch.object(TempoServer, "get") + def test_get_event(self, mock_get, tempo_server): + """Test get_event method.""" + mock_get.return_value = {"id": 1, "type": "worklog_created"} + result = tempo_server.events.get_event(1) + mock_get.assert_called_once_with("1", **{}) + assert result == {"id": 1, "type": "worklog_created"}