From 84690ed2ba86fdc87bd5211aad8812c202795731 Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:27:30 -0500 Subject: [PATCH 1/7] docs: add reports dir --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 59ac872..1e6bf98 100644 --- a/.gitignore +++ b/.gitignore @@ -217,4 +217,5 @@ replay_pid* *.code-workspace # Directories -*reference* \ No newline at end of file +*reference* +*reports* \ No newline at end of file From 4be67d167945cd4e484d999de7c9a2e0e5d22ff1 Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:28:03 -0500 Subject: [PATCH 2/7] docs: add pandas for dataframe manipulation --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5bd675c..5b82e8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests pytest python-dotenv -matplotlib \ No newline at end of file +matplotlib +pandas \ No newline at end of file From 4f389b6ca293422c75abe89f0c1c43b7da3b17a2 Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:28:30 -0500 Subject: [PATCH 3/7] feat: create endpoints for changes --- smartpm/endpoints/changes.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 smartpm/endpoints/changes.py diff --git a/smartpm/endpoints/changes.py b/smartpm/endpoints/changes.py new file mode 100644 index 0000000..2a0a793 --- /dev/null +++ b/smartpm/endpoints/changes.py @@ -0,0 +1,48 @@ +from smartpm.client import SmartPMClient +from smartpm.decorators import api_wrapper, utility +from smartpm.utils import plot_schedule_changes +from smartpm.logging_config import logger + +class Changes: + def __init__(self, client: SmartPMClient): + self.client = client + + @api_wrapper + def get_changes_summary(self, project_id, scenario_id): + """ + Retrieve summary information about the changes that have happened to a scenario over time: https://developers.smartpmtech.com/#operation/get-change-log-sumamry + + Parameters + ---------- + project_id : str + The Project ID containing the scenario for which you would like to pull the changes summary from + scenario_id : str + The Scenario ID for which you would like to pull the changes summary from + Returns + ------- + : dict + Changes summary data as a JSON object + """ + logger.debug(f"Fetching changes summary for project_id: {project_id}, scenario_id: {scenario_id}") + + endpoint = f'v1/projects/{project_id}/scenarios/{scenario_id}/change-log-summary' + response = self.client._get(endpoint=endpoint) + + return response + + @utility + def plot_changes_summary(self, project_id, scenario_id): + """ + Retrieve the changes summary data and plot it. + Reproduces this figure from SmartPM: https://help.smartpmtech.com/trends-schedule-changes-over-time + + 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 changes summary for project_id: {project_id}, scenario_id: {scenario_id}") + curve_data = self.get_changes_summary(project_id, scenario_id) + plot_schedule_changes(curve_data) \ No newline at end of file From bde7bf2a4f168bd2ca0a436cc92f26185ce8b180 Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:29:59 -0500 Subject: [PATCH 4/7] feat: add fxn to create projects dataframe --- smartpm/endpoints/projects.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/smartpm/endpoints/projects.py b/smartpm/endpoints/projects.py index 8978fc1..ffa5888 100644 --- a/smartpm/endpoints/projects.py +++ b/smartpm/endpoints/projects.py @@ -1,6 +1,10 @@ +import pandas as pd + from smartpm.client import SmartPMClient from smartpm.decorators import api_wrapper, utility from smartpm.logging_config import logger + + class Projects: def __init__(self, client: SmartPMClient): self.client = client @@ -94,4 +98,31 @@ def find_project_by_name(self, name): return project logger.info(f"Project with name '{name}' not found.") - return None \ No newline at end of file + return None + + @utility + def get_projects_dataframe(self): + """ + Get all projects and return as a DataFrame with selected columns. + + Returns + ------- + pd.DataFrame + DataFrame containing projects data with selected columns. + """ + projects = self.get_projects() + + # Extract relevant fields and metadata + project_data = [] + for project in projects: + project_data.append({ + "id": project["id"], + "name": project["name"], + "startDate": project["startDate"], + "city": project["city"], + "projectNumber": project["metadata"].get("PROJECT_NUMBER"), + "region": project["metadata"].get("REGION") + }) + + df = pd.DataFrame(project_data, columns=["id", "name", "startDate", "city", "projectNumber", "region"]) + return df \ No newline at end of file From 8841c630bb61440c7dafe7c1100f24913d1b2527 Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:30:25 -0500 Subject: [PATCH 5/7] feat: create fxn to plot schedule changes over time --- smartpm/utils.py | 70 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/smartpm/utils.py b/smartpm/utils.py index 8e817f9..52c4d82 100644 --- a/smartpm/utils.py +++ b/smartpm/utils.py @@ -141,9 +141,6 @@ def plot_earned_schedule_curve(json_data): plt.tight_layout() plt.show() -import matplotlib.pyplot as plt -import datetime - def plot_schedule_delay(json_data): """ Plot the schedule delay curve from the provided JSON data. @@ -200,3 +197,70 @@ def plot_schedule_delay(json_data): plt.tight_layout() plt.show() + +def plot_schedule_changes(json_data): + """ + Plot the schedule changes over time from the provided JSON data. + + Parameters + ---------- + json_data : list of dict + List of dictionaries containing the schedule change data. + """ + # Extract dates and metrics + dates = [datetime.datetime.strptime(entry['dataDate'], '%Y-%m-%dT%H:%M:%S') for entry in json_data] + + metrics = json_data[0]['metrics'].keys() + + # Initialize a dictionary to hold lists of metric values + metric_values = {metric: [] for metric in metrics} + + # Populate the metric values dictionary + for entry in json_data: + for metric in metrics: + metric_values[metric].append(entry['metrics'][metric]) + + # Define colors and linestyles for each metric + color_linestyle_dict = { + "CriticalChanges": {"color": 'red', "marker": 'o'}, + "NearCriticalChanges": {"color": 'goldenrod', "marker": 'd'}, + "ActivityChanges": {"color": 'seagreen', "marker": 's'}, + "LogicChanges": {"color": 'blue', "marker": '^'}, + "CalendarChanges": {"color": 'dodgerblue', "marker": 'v'}, + "DurationChanges": {"color": 'steelblue', "marker": 'o'}, + "DelayedActivityChanges": {"color": 'firebrick', "marker": 'd'}, + } + + plt.figure(figsize=(14, 10)) + ms = 4 # marker size + lw = 2 # linewidth + + for metric, style in color_linestyle_dict.items(): + plt.plot(dates, metric_values[metric], label=metric, marker=style["marker"], markersize=ms, linewidth=lw, color=style["color"]) + + plt.title('Schedule Changes Over Time') + + # 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=4, frameon=False) + plt.grid(True) + + plt.tight_layout() + plt.show() \ No newline at end of file From 09150d0c872e23ef12ea77722ef1ff78be925aa9 Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:31:46 -0500 Subject: [PATCH 6/7] hotfix: fix call to fxn --- snippets/plot_progress.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snippets/plot_progress.py b/snippets/plot_progress.py index 27782e4..7776e69 100644 --- a/snippets/plot_progress.py +++ b/snippets/plot_progress.py @@ -52,7 +52,7 @@ def main(): ) print(json.dumps(complete_curve["data"][0], indent=4)) - scenarios_api.plot_scenario_progress( + scenarios_api.plot_percent_complete_curve( project_id=project_id, scenario_id=scenario_id ) From c63627dc92e35140d7a2e5b7786ecc5b2cac7eab Mon Sep 17 00:00:00 2001 From: Hagen Fritz Date: Tue, 28 May 2024 10:32:04 -0500 Subject: [PATCH 7/7] feat: showcase use of changes endpoint --- snippets/explore_changes.py | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 snippets/explore_changes.py diff --git a/snippets/explore_changes.py b/snippets/explore_changes.py new file mode 100644 index 0000000..c3b25eb --- /dev/null +++ b/snippets/explore_changes.py @@ -0,0 +1,58 @@ +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 +from smartpm.endpoints.changes import Changes + +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) + scenarios_api = Scenarios(client) + changes_api = Changes(client) + + # Get Changes Summary + # ------------------- + # Find project by name + name_to_find = "212096 - 401 FIRST STREET (College Station)" # replace with your project name + project = projects_api.find_project_by_name(name=name_to_find) + project_id = project["id"] + + # Find scenario by name + scenario_to_find = "Full Schedule" # replace with your scenario name + matching_scenarios = scenarios_api.find_scenario_by_name( + project_id=project_id, + scenario_name=scenario_to_find + ) + scenario_id = matching_scenarios[-1].get("id") + + print("Get Summary Changes") + changes_summary = changes_api.get_changes_summary( + project_id=project_id, + scenario_id=scenario_id + ) + print("Example changes summary entry:") + print(json.dumps(changes_summary[0], indent=4)) + # ------------------- + + # Plot Changes Summary + # -------------------- + changes_api.plot_changes_summary( + project_id=project_id, + scenario_id=scenario_id + ) + +if __name__ == "__main__": + main()