From c63595c9fa7958f02900cd80429e687889df13a9 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sun, 2 Nov 2025 12:42:14 +0100 Subject: [PATCH] Added Project class. --- devops.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++------ harvester.py | 15 +++++++------- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/devops.py b/devops.py index c93c363..3eff05d 100644 --- a/devops.py +++ b/devops.py @@ -1,29 +1,74 @@ +from unicodedata import name import requests import urllib.request import urllib.parse import json +from uuid import UUID DEVOPS_SCOPE = "https://app.vssps.visualstudio.com/.default" DEVOPS_API_VERSION = "7.1" -class Client: +class Organization: def __init__(self, org_url: str, token: str, api_version: str = DEVOPS_API_VERSION): self._org_url = org_url.rstrip("/") + "/" # Ensure trailing slash self._token = token self._api_version = api_version def get(self, path: str, params: dict = {}): - p = { + request_parameters = { "api-version": self._api_version, **params } - url = self._org_url + path.lstrip("/") # Ensure single slash between base and path - r = requests.get(url=url, params=p, headers={ + encoded_path = urllib.parse.quote(path.lstrip("/")) # Ensure single slash between base and path + url = self._org_url + encoded_path + r = requests.get(url=url, params=request_parameters, headers={ "Authorization": f"Bearer {self._token}" }) return r - def get_projects(self): + @property + def projects(self): r = self.get("_apis/projects") r.raise_for_status() - return r.json() + + # Return a list of Project instances + projects_data = r.json().get("value", []) + return [ + Project(self, + id=proj.get("id"), + name=proj.get("name"), + url=proj.get("url"), + description=proj.get("description"), + ) for proj in projects_data + ] + +class Project: + def __init__(self, org: Organization, id: str, name: str | None = None, url: str | None = None, description: str | None = None): + self._org = org + + # Check, if the id is a valid UUID + try: + self._id = str(UUID(id)) + except ValueError: + raise ValueError(f"Invalid project ID: {self._id}") + + if name is not None and url is not None and name != "" and url != "": + self._name = name + self._url = url + self._description = description if description is not None else "" # Ensure description is a string + else: + # Fetch project details from the API + r = org.get(f"_apis/projects/{urllib.parse.quote(self._id)}") + r.raise_for_status() + proj_data = r.json() + self._id = proj_data.get("id", "") + self._name = proj_data.get("name", "") + self._url = proj_data.get("url", "") + self._description = proj_data.get("description", "") + + @property + def name(self): + return self._name + + def __str__(self): + return f"Project(name={self._name}, id={self._id})" diff --git a/harvester.py b/harvester.py index 496ede1..c26ed26 100755 --- a/harvester.py +++ b/harvester.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 -from devops import Client, DEVOPS_SCOPE +from devops import Organization, Project, DEVOPS_SCOPE from azure.identity import DefaultAzureCredential from json import dumps -def main(): - token = DefaultAzureCredential().get_token(DEVOPS_SCOPE).token - client = Client("https://dev.azure.com/mcovsandbox", token) - projects = client.get_projects() - print(dumps(projects, indent=2)) +org = Organization("https://dev.azure.com/mcovsandbox", DefaultAzureCredential().get_token(DEVOPS_SCOPE).token) +projects = org.projects +print([str(p) for p in projects]) -if __name__ == "__main__": - main() +ado_sandbox = Project(org, id="bafe0cf1-6c97-4088-864a-ea6dc02b2727") + +print(ado_sandbox)