diff --git a/.gitignore b/.gitignore index df9d453..59ac872 100644 --- a/.gitignore +++ b/.gitignore @@ -215,3 +215,6 @@ replay_pid* # VS Code *.code-workspace + +# Directories +*reference* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1bed588..5bd675c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests pytest -python-dotenv \ No newline at end of file +python-dotenv +matplotlib \ No newline at end of file diff --git a/smartpm/client.py b/smartpm/client.py index efbb790..3bc0a13 100644 --- a/smartpm/client.py +++ b/smartpm/client.py @@ -2,7 +2,7 @@ from smartpm.exceptions import SmartPMError, AuthenticationError, NotFoundError, RateLimitExceededError, BadRequestError, NoCommentsFoundError class SmartPMClient: - BASE_URL = 'https://live.smartpmtech.com/public/v1' + BASE_URL = 'https://live.smartpmtech.com/public' def __init__(self, api_key, company_id): self.api_key = api_key diff --git a/smartpm/decorators.py b/smartpm/decorators.py new file mode 100644 index 0000000..00fb04c --- /dev/null +++ b/smartpm/decorators.py @@ -0,0 +1,19 @@ +import functools + +def api_wrapper(func): + """Decorator to mark API wrapper functions.""" + @functools.wraps(func) + def wrapper_api_wrapper(*args, **kwargs): + # You can add common functionality here, such as logging + print(f"Calling API wrapper function: {func.__name__}") + return func(*args, **kwargs) + return wrapper_api_wrapper + +def utility(func): + """Decorator to mark utility functions.""" + @functools.wraps(func) + def wrapper_utility(*args, **kwargs): + # You can add common functionality here, such as logging + print(f"Calling utility function: {func.__name__}") + return func(*args, **kwargs) + return wrapper_utility diff --git a/smartpm/endpoints/projects.py b/smartpm/endpoints/projects.py index f9d9ad4..c980f20 100644 --- a/smartpm/endpoints/projects.py +++ b/smartpm/endpoints/projects.py @@ -1,9 +1,11 @@ from smartpm.client import SmartPMClient - +from smartpm.decorators import api_wrapper +from smartpm.logging_config import logger class Projects: def __init__(self, client: SmartPMClient): self.client = client + @api_wrapper def get_projects(self, as_of=None): """ Access basic project data: https://developers.smartpmtech.com/#operation/get-projects @@ -19,12 +21,16 @@ def get_projects(self, as_of=None): : list of dict projects data as a JSON object """ + logger.debug(f"Fetching projects as of: {as_of}") params = {} if as_of: params['asOf'] = as_of - return self.client._get(endpoint='projects', params=params) - + endpoint = 'v1/projects' + response = self.client._get(endpoint=endpoint, params=params) + return response + + @api_wrapper def get_project(self, project_id): """ Get a specific project by its ID: https://developers.smartpmtech.com/#operation/get-project @@ -39,10 +45,12 @@ def get_project(self, project_id): : dict project details as a JSON object """ - endpoint = f'projects/{project_id}' + logger.debug(f"Fetching project with ID: {project_id}") + endpoint = f'v1/projects/{project_id}' + response = self.client._get(endpoint=endpoint) + return response - return self.client._get(endpoint) - + @api_wrapper def get_project_comments(self, project_id): """ Get comments for a specific project: https://developers.smartpmtech.com/#operation/get-project-comments @@ -57,6 +65,7 @@ def get_project_comments(self, project_id): : list of dict project comments as a JSON object """ - endpoint = f'projects/{project_id}/comments' - - return self.client._get(endpoint) \ No newline at end of file + logger.debug(f"Fetching comments for project ID: {project_id}") + endpoint = f'v1/projects/{project_id}/comments' + response = self.client._get(endpoint=endpoint) + return response diff --git a/smartpm/endpoints/scenarios.py b/smartpm/endpoints/scenarios.py new file mode 100644 index 0000000..e1613cf --- /dev/null +++ b/smartpm/endpoints/scenarios.py @@ -0,0 +1,146 @@ +from smartpm.client import SmartPMClient +from smartpm.utils import plot_percent_complete_curve, plot_earned_schedule_curve +from smartpm.decorators import api_wrapper, utility +from smartpm.logging_config import logger + +class Scenarios: + def __init__(self, client: SmartPMClient): + self.client = client + + @api_wrapper + def get_scenarios(self, project_id, as_of=None): + """ + Get scenarios for a specific project: https://developers.smartpmtech.com/#operation/get-scenarios + + Parameters + ---------- + project_id : int + ID of the project to retrieve scenarios for + as_of : str, default None + Return scenarios that have changed since date in format `2023-07-19T12:00:00` + + Returns + ------- + : list of dict + project scenarios as a JSON object + """ + logger.debug(f"Fetching scenarios for project_id: {project_id}, as_of: {as_of}") + params = {} + if as_of: + params['asOf'] = as_of + + endpoint = f'v1/projects/{project_id}/scenarios' + + return self.client._get(endpoint=endpoint, params=params) + + @api_wrapper + def get_scenario_details(self, project_id, scenario_id, data_date=None): + """ + Get the details for a specific scenario: https://developers.smartpmtech.com/#operation/get-scenario-details + + Parameters + ---------- + project_id : int + ID of the project containing the scenario + scenario_id : int + ID of the scenario to retrieve details for + data_date : str, default None + Data date in format `yyyy-MM-dd` for which to retrieve the scenario details + If None, will use the latest data date + + Returns + ------ + : dict + scenario details as a JSON object + """ + logger.debug(f"Fetching scenario details for project_id: {project_id}, scenario_id: {scenario_id}, data_date: {data_date}") + params = {} + if data_date: + params['dataDate'] = data_date + + endpoint = f'v1/projects/{project_id}/scenarios/{scenario_id}' + return self.client._get(endpoint=endpoint, params=params) + + @api_wrapper + def get_percent_complete_curve(self, project_id, scenario_id, delta=False): + """ + Get Percent Complete Curve data for a specific scenario: https://developers.smartpmtech.com/#operation/get-percent-complete-curve + + Parameters + ---------- + project_id : str + ID of the project containing the scenario + scenario_id : str + ID of the scenario to retrieve the percent complete curve for + delta : bool, default False + Return the change of progress between periods if True + + Returns + ------- + : dict + percent complete curve data as a JSON object + """ + logger.debug(f"Fetching percent complete curve data for project_id: {project_id}, scenario_id: {scenario_id}, delta: {delta}") + params = {'delta': str(delta).lower()} + + endpoint = f'v2/projects/{project_id}/scenarios/{scenario_id}/percent-complete-curve' + return self.client._get(endpoint=endpoint, params=params) + + @utility + def plot_percent_complete_curve(self, project_id, scenario_id, delta=False): + """ + Retrieve the percent complete curve and plot the progress. + Reproduces this figure from SmartPM: https://help.smartpmtech.com/the-progress-curve + + Parameters + ---------- + project_id : str + ID of the project containing the scenario + scenario_id : str + ID of the scenario to retrieve the percent complete curve for + delta : bool, default False + Return the change of progress between periods if True + """ + logger.debug(f"Plotting scenario progress for project_id: {project_id}, scenario_id: {scenario_id}, delta: {delta}") + curve_data = self.get_percent_complete_curve(project_id, scenario_id, delta) + plot_percent_complete_curve(curve_data) + + @api_wrapper + def get_earned_schedule_curve(self, project_id, scenario_id): + """ + Get Earned Schedule Curve for a specific scenario. + + Parameters + ---------- + project_id : str + ID of the project containing the scenario + scenario_id : str + ID of the scenario to retrieve the earned schedule curve for + + Returns + ------- + : dict + earned schedule curve data as a JSON object + """ + logger.debug(f"Fetching earned schedule curve for project_id: {project_id}, scenario_id: {scenario_id}") + endpoint = f'v1/projects/{project_id}/scenarios/{scenario_id}/earned-schedule-curve' + response = self.client._get(endpoint=endpoint) + + return response + + @utility + def plot_earned_schedule_curve(self, project_id, scenario_id): + """ + Retrieve the earned days curve and plot the results + Reproduces this figure: https://help.smartpmtech.com/earned-baseline-days + + Parameters + ---------- + project_id : str + ID of the project containing the scenario + scenario_id : str + ID of the scenario to retrieve the percent complete curve for + """ + logger.debug(f"Plotting earned schedule curve for project_id: {project_id}, scenario_id: {scenario_id}") + earned_days_data = self.get_earned_schedule_curve(project_id, scenario_id) + plot_earned_schedule_curve(earned_days_data) \ No newline at end of file diff --git a/smartpm/logging_config.py b/smartpm/logging_config.py new file mode 100644 index 0000000..805a1e1 --- /dev/null +++ b/smartpm/logging_config.py @@ -0,0 +1,16 @@ +import logging + +# Create a logger +logger = logging.getLogger('smartpm') +logger.setLevel(logging.DEBUG) + +# Create console handler and set level to debug +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) + +# Create formatter and add it to the handler +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) + +# Add the handler to the logger +logger.addHandler(ch) diff --git a/smartpm/utils.py b/smartpm/utils.py new file mode 100644 index 0000000..e198ce7 --- /dev/null +++ b/smartpm/utils.py @@ -0,0 +1,142 @@ +import matplotlib.pyplot as plt +import datetime + +def plot_percent_complete_curve(json_data): + """ + Plot the progress curve from scenario data. + Reproduces this figure from SmartPM: https://help.smartpmtech.com/the-progress-curve + + Parameters + ---------- + json_data : dict + Dictionary containing percent complete types and scenario data with progress information. + """ + percent_complete_types = json_data['percentCompleteTypes'] + scenario_data = json_data['data'] + + # Extract dates and values for each type of progress + dates = [datetime.datetime.strptime(point['DATE'], '%Y-%m-%d') for point in scenario_data] + + # Extract data points for each type based on percentCompleteTypes + data_series = {key: [point.get(key, None) for point in scenario_data] for key in percent_complete_types.keys()} + + # Define colors and linestyles for each type + colors = { + "ACTUAL": "steelblue", + "SCHEDULED": "seagreen", + "PLANNED": "seagreen", + "PREDICTIVE": "steelblue", + "LATE_DATE_PLANNED": "firebrick", + } + markers = { + "ACTUAL": "s", + "SCHEDULED": "^", + "PLANNED": "o", + "PREDICTIVE": "v", + "LATE_DATE_PLANNED": "d", + } + + linestyles = { + "ACTUAL": "-", + "SCHEDULED": "--", + "PLANNED": "-", + "PREDICTIVE": "--", + "LATE_DATE_PLANNED": "-", + } + + plt.figure(figsize=(14, 6)) + ms = 4 # marker size + lw = 2 # linewidth + + # Plot each data series + for key, label in percent_complete_types.items(): + if any(data_series[key]): + plt.plot(dates, data_series[key], label=label.split(" (")[0], marker=markers.get(key, 'o'), markersize=ms, linestyle=linestyles.get(key, '-'), linewidth=lw, color=colors.get(key, 'dodgerblue')) + + # Shade the region between early date planned and late date planned lines if available + if 'PLANNED' in data_series and 'LATE_DATE_PLANNED' in data_series: + plt.fill_between(dates, data_series['PLANNED'], data_series.get('LATE_DATE_PLANNED', [None]*len(dates)), color='gray', alpha=0.3) + + plt.title('Planned VS Actual Percent Complete') + + # Customize x-axis to show the first of every month with the format "mm/dd/yy" + plt.gca().xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%m/%d/%y')) + plt.gca().xaxis.set_major_locator(plt.matplotlib.dates.MonthLocator(bymonthday=1, interval=2)) + + # Rotate the x-axis labels by -30 degrees and align them to the left + plt.xticks(rotation=-30, ha='left') + + # Customize y-axis to show 0, 25, 50, 75, 100 + plt.yticks([0, 25, 50, 75, 100]) + + # Remove top, right, and left spines + ax = plt.gca() + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + + # Draw a vertical line for the current date if it is before the last date in predictive completion + current_date = datetime.datetime.now() + if current_date < max(dates): + plt.axvline(x=current_date, color='black', linestyle='-', linewidth=lw + 1, label='Current Date') + + # Place the legend below the x-axis with no box around it on one line + plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=7, frameon=False) + plt.grid(True) + + plt.tight_layout() + plt.show() + +def plot_earned_schedule_curve(json_data): + """ + Plot the earned days curve from the provided JSON data. + Reproduces this figure: https://help.smartpmtech.com/earned-baseline-days + + Parameters + ---------- + json_data : dict + Dictionary containing the date and days data. + """ + data = json_data['data'] + + # Extract dates and values for each type of days + dates = [datetime.datetime.strptime(point['date'], '%Y-%m-%d') for point in data] + earned_days = [point['earnedDays'] for point in data] + planned_days = [point['plannedDays'] for point in data] + predictive_days = [point['predictiveDays'] if point['predictiveDays'] is not None else None for point in data] + + plt.figure(figsize=(14, 6)) + ms = 4 # marker size + lw = 2 # linewidth + + plt.plot(dates, earned_days, label='Earned Days', marker='o', markersize=ms, linestyle='-', linewidth=lw, color="seagreen") + plt.plot(dates, planned_days, label='Planned Days', marker='d', markersize=ms, linestyle='-', linewidth=lw, color="firebrick") + if any(predictive_days): + plt.plot(dates, predictive_days, label='Predictive Days', marker='s', markersize=ms, linestyle='-', linewidth=lw, color="goldenrod") + + plt.title('Earned Baseline Days') + + # Customize x-axis to show the first of every month with the format "mm/dd/yy" + plt.gca().xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%m/%d/%y')) + plt.gca().xaxis.set_major_locator(plt.matplotlib.dates.MonthLocator(bymonthday=1, interval=2)) + + # Rotate the x-axis labels by -30 degrees and align them to the left + plt.xticks(rotation=-30, ha='left') + + # Remove top, right, and left spines + ax = plt.gca() + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + + # Draw a vertical line for the current date if it is before the last date in data + current_date = datetime.datetime.now() + if current_date < max(dates): + plt.axvline(x=current_date, color='black', linestyle='-', linewidth=lw + 1, label='Current Date') + + # Place the legend below the x-axis with no box around it on one line + plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=7, frameon=False) + plt.grid(True) + + plt.tight_layout() + plt.show() \ No newline at end of file diff --git a/snippets/list_projects.py b/snippets/explore_projects.py similarity index 100% rename from snippets/list_projects.py rename to snippets/explore_projects.py diff --git a/snippets/explore_scenarios.py b/snippets/explore_scenarios.py new file mode 100644 index 0000000..f62e290 --- /dev/null +++ b/snippets/explore_scenarios.py @@ -0,0 +1,93 @@ +import os +import sys +import json + +# Add the package root directory to the sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) + +from dotenv import load_dotenv +load_dotenv() # Load environment variables from .env file + +from smartpm.client import SmartPMClient +from smartpm.endpoints.projects import Projects # import projects to get project IDs +from smartpm.endpoints.scenarios import Scenarios + +API_KEY = os.getenv("API_KEY") +COMPANY_ID = os.getenv("COMPANY_ID") + +def main(): + # Setup SDK + client = SmartPMClient(API_KEY, COMPANY_ID) + projects_api = Projects(client) # see snippets/list_projects.py for more use + scenarios_api = Scenarios(client) + + # Get All Scenarios + # ----------------- + project_idx = 0 # change to explore explore different projects + # Start by getting projects + print("Get All Projects") + projects = projects_api.get_projects() + + # Get scenarios by supplying the project ID from any project, in this case the first project in the list + project_id = projects[project_idx]['id'] + print(f"Get All Scenarios for project: {project_id}") + scenarios = scenarios_api.get_scenarios(project_id=project_id) + print("Last Scenario: Summary") + print(json.dumps(scenarios[-1], indent=4)) # view the first scenario + # ----------------- + + # View Scenario Details + # --------------------- + # Get latest scenario for "Full Schedule" + scenario_id = scenarios[0]["id"] # default to the first scenario in the list + for scenario in scenarios: + if scenario["name"] == "Full Schedule": + scenario_id = scenario["id"] + print("Scenario Summay: 'Full Schedule'") + print(json.dumps(scenario, indent=4)) + + print(f"Getting scenario details for Scenario {scenario_id}") + scenario_details = scenarios_api.get_scenario_details( + project_id=project_id, + scenario_id=scenario_id + ) + print("Last Scenario: Details") + print(json.dumps(scenario_details, indent=4)) + # --------------------- + + # Get Percent Complete Curve + # -------------------------- + data = scenarios_api.get_percent_complete_curve( + project_id=project_id, + scenario_id=scenario_id, + delta=False + ) + print("Percent Complete Types:") + print(data["percentCompleteTypes"]) + print(f"Number of datapoints: {len(data['data'])}") + print("Example data entry:") + print(json.dumps(data["data"][0], indent=4)) + # Plot the curves + scenarios_api.plot_percent_complete_curve( + project_id=project_id, + scenario_id=scenario_id + ) + # -------------------------- + + # Get Earned Schedule + # ------------------- + earned_schedule_data = scenarios_api.get_earned_schedule_curve( + project_id=project_id, + scenario_id=scenario_id + ) + print("Example data entry:") + print(json.dumps(earned_schedule_data["data"][0], indent=4)) + # Plot the curves + scenarios_api.plot_earned_schedule_curve( + project_id=project_id, + scenario_id=scenario_id + ) + # ------------------- + +if __name__ == "__main__": + main() diff --git a/snippets/plot_earned_days.py b/snippets/plot_earned_days.py new file mode 100644 index 0000000..4df779f --- /dev/null +++ b/snippets/plot_earned_days.py @@ -0,0 +1,61 @@ +import os +import sys +import json + +# Add the package root directory to the sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) + +from dotenv import load_dotenv +load_dotenv() # Load environment variables from .env file + +from smartpm.client import SmartPMClient +from smartpm.endpoints.projects import Projects # import projects to get project IDs +from smartpm.endpoints.scenarios import Scenarios + +API_KEY = os.getenv("API_KEY") +COMPANY_ID = os.getenv("COMPANY_ID") + +def main(): + # Setup SDK + client = SmartPMClient(API_KEY, COMPANY_ID) + projects_api = Projects(client) # see snippets/list_projects.py for more use + scenarios_api = Scenarios(client) # see snippets/list_scenarios.py for more use + + # Plot Planned vs Actual Percent Complete for Full Schedule + # --------------------------------------------------------- + # Find project by name + name_to_find = "212096 - 401 FIRST STREET (College Station)" # replace with your project name + projects = projects_api.get_projects() + project_id = projects[0]["id"] # default to first project + for project in projects: + if project["name"] == name_to_find: + print(f"Found Project: {project['id']} - {project['name']}") + project_id = project["id"] + break + + # Find scenario by name + scenario_to_find = "Full Schedule" # replace with your scenario name + scenarios = scenarios_api.get_scenarios(project_id=project_id) + print(json.dumps(scenarios, indent=4)) + scenario_id = scenarios[0]["id"] # default to first scenario + for scenario in scenarios: + if scenario["name"] == scenario_to_find: + print(f"Found Scenario:") + print(json.dumps(scenario, indent=4)) + scenario_id = scenario["id"] + # don't break since there are might be multiple matching scenarios and we want to get the latest + + # Check data + complete_curve = scenarios_api.get_earned_schedule_curve( + project_id=project_id, + scenario_id=scenario_id + ) + print(json.dumps(complete_curve["data"][0], indent=4)) + + scenarios_api.plot_earned_schedule_curve( + project_id=project_id, + scenario_id=scenario_id + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/snippets/plot_progress.py b/snippets/plot_progress.py new file mode 100644 index 0000000..27782e4 --- /dev/null +++ b/snippets/plot_progress.py @@ -0,0 +1,61 @@ +import os +import sys +import json + +# Add the package root directory to the sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) + +from dotenv import load_dotenv +load_dotenv() # Load environment variables from .env file + +from smartpm.client import SmartPMClient +from smartpm.endpoints.projects import Projects # import projects to get project IDs +from smartpm.endpoints.scenarios import Scenarios + +API_KEY = os.getenv("API_KEY") +COMPANY_ID = os.getenv("COMPANY_ID") + +def main(): + # Setup SDK + client = SmartPMClient(API_KEY, COMPANY_ID) + projects_api = Projects(client) # see snippets/list_projects.py for more use + scenarios_api = Scenarios(client) # see snippets/list_scenarios.py for more use + + # Plot Planned vs Actual Percent Complete for Full Schedule + # --------------------------------------------------------- + # Find project by name + name_to_find = "212096 - 401 FIRST STREET (College Station)" # replace with your project name + projects = projects_api.get_projects() + project_id = projects[0]["id"] # default to first project + for project in projects: + if project["name"] == name_to_find: + print(f"Found Project: {project['id']} - {project['name']}") + project_id = project["id"] + break + + # Find scenario by name + scenario_to_find = "Full Schedule" # replace with your scenario name + scenarios = scenarios_api.get_scenarios(project_id=project_id) + print(json.dumps(scenarios, indent=4)) + scenario_id = scenarios[0]["id"] # default to first scenario + for scenario in scenarios: + if scenario["name"] == scenario_to_find: + print(f"Found Scenario:") + print(json.dumps(scenario, indent=4)) + scenario_id = scenario["id"] + # don't break since there are might be multiple matching scenarios and we want to get the latest + + # Check data + complete_curve = scenarios_api.get_percent_complete_curve( + project_id=project_id, + scenario_id=scenario_id + ) + print(json.dumps(complete_curve["data"][0], indent=4)) + + scenarios_api.plot_scenario_progress( + project_id=project_id, + scenario_id=scenario_id + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 0000000..8908a1c --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,152 @@ +import pytest +import os +import sys +import logging +import json + +# Add the package root directory to the sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) + +from dotenv import load_dotenv +load_dotenv() # Load environment variables from .env file + +from smartpm.client import SmartPMClient +from smartpm.endpoints.scenarios import Scenarios +from smartpm.endpoints.projects import Projects + +API_KEY = os.getenv("API_KEY") +COMPANY_ID = os.getenv("COMPANY_ID") + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +@pytest.fixture +def client(): + return SmartPMClient(API_KEY, COMPANY_ID) + +@pytest.fixture +def scenarios(client): + return Scenarios(client) + +@pytest.fixture +def projects(client): + return Projects(client) + +def test_get_scenarios(scenarios, projects): + """Test retrieving scenarios for a specific project by its ID.""" + # Get a list of projects to use a valid project ID + all_projects = projects.get_projects() + logger.info("Number of projects: %d", len(all_projects)) + + assert isinstance(all_projects, list) + assert len(all_projects) > 0 + + first_project = all_projects[0] + project_id = first_project['id'] + + # Retrieve scenarios for the first project + project_scenarios = scenarios.get_scenarios(project_id) + logger.info("Project ID: %s", project_id) + + # Pretty-print the project scenarios + pretty_scenarios = json.dumps(project_scenarios, indent=4) + logger.info("Project Scenarios: %s", pretty_scenarios) + + assert isinstance(project_scenarios, list) + +def test_get_scenario_details(scenarios, projects): + """Test retrieving details for a specific scenario by its ID.""" + # Get a list of projects to use a valid project ID + all_projects = projects.get_projects() + logger.info("Number of projects: %d", len(all_projects)) + + assert isinstance(all_projects, list) + assert len(all_projects) > 0 + + first_project = all_projects[0] + project_id = first_project['id'] + + # Retrieve scenarios for the first project + project_scenarios = scenarios.get_scenarios(project_id) + logger.info("Project ID: %s", project_id) + + assert isinstance(project_scenarios, list) + assert len(project_scenarios) > 0 + + first_scenario = project_scenarios[0] + scenario_id = first_scenario['id'] + + # Retrieve scenario details for the first scenario + scenario_details = scenarios.get_scenario_details(project_id, scenario_id) + logger.info("Scenario ID: %s", scenario_id) + + # Pretty-print the scenario details + pretty_details = json.dumps(scenario_details, indent=4) + logger.info("Scenario Details: %s", pretty_details) + + assert isinstance(scenario_details, dict) + +def test_get_percent_complete_curve(scenarios, projects): + """Test retrieving percent complete curve for a specific scenario by its ID.""" + # Get a list of projects to use a valid project ID + all_projects = projects.get_projects() + logger.info("Number of projects: %d", len(all_projects)) + + assert isinstance(all_projects, list) + assert len(all_projects) > 0 + + first_project = all_projects[0] + project_id = first_project['id'] + + # Retrieve scenarios for the first project + project_scenarios = scenarios.get_scenarios(project_id) + logger.info("Project ID: %s", project_id) + + assert isinstance(project_scenarios, list) + assert len(project_scenarios) > 0 + + first_scenario = project_scenarios[0] + scenario_id = first_scenario['id'] + + # Retrieve percent complete curve for the first scenario + percent_complete_curve = scenarios.get_percent_complete_curve(project_id, scenario_id) + logger.info("Scenario ID: %s", scenario_id) + + # Pretty-print the percent complete curve + pretty_curve = json.dumps(percent_complete_curve, indent=4) + logger.info("Percent Complete Curve: %s", pretty_curve) + + assert isinstance(percent_complete_curve, dict) + +def test_get_earned_schedule_curve(scenarios, projects): + """Test retrieving earned schedule curve for a specific scenario by its ID.""" + # Get a list of projects to use a valid project ID + all_projects = projects.get_projects() + logger.info("Number of projects: %d", len(all_projects)) + + assert isinstance(all_projects, list) + assert len(all_projects) > 0 + + first_project = all_projects[0] + project_id = first_project['id'] + + # Retrieve scenarios for the first project + project_scenarios = scenarios.get_scenarios(project_id) + logger.info("Project ID: %s", project_id) + + assert isinstance(project_scenarios, list) + assert len(project_scenarios) > 0 + + first_scenario = project_scenarios[0] + scenario_id = first_scenario['id'] + + # Retrieve earned schedule curve for the first scenario + earned_schedule_curve = scenarios.get_earned_schedule_curve(project_id, scenario_id) + logger.info("Scenario ID: %s", scenario_id) + + # Pretty-print the earned schedule curve + pretty_curve = json.dumps(earned_schedule_curve, indent=4) + logger.info("Earned Schedule Curve: %s", pretty_curve) + + assert isinstance(earned_schedule_curve, dict) \ No newline at end of file