Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
db730d6
Add license details in deal assistant
projjal1611 May 31, 2026
633c070
Fix filename
projjal1611 May 31, 2026
38401b3
Add permissions details in Readme
projjal1611 May 31, 2026
f832107
Add dummy readme
projjal1611 Jun 1, 2026
dd49371
Test commit to README
NityamPareek Jun 1, 2026
4756f47
feat: implement modular SharePoint & OneDrive telemetry frame
srishti-negi Jun 1, 2026
6bbc5e9
refactor: eliminate duplicated auth, download, and concurrency logic …
srishti-negi Jun 1, 2026
f353e5e
refactor: centralize Material 3 style constants and typography tokens…
srishti-negi Jun 1, 2026
13cdd0a
refactor: integrate unified core/graph Microsoft Graph API service cl…
srishti-negi Jun 1, 2026
9f0f72b
refactor: unify and isolate all Microsoft Graph API endpoints and URL…
srishti-negi Jun 1, 2026
6b76eff
fix: trigger GraphClient authenticate inside telemetry pipelines to p…
srishti-negi Jun 1, 2026
131d0a2
refactor: integrate unified core/graph GraphClient inside EstimatorFa…
srishti-negi Jun 1, 2026
2b68097
Merge pull request #22 from srishti-negi/splash-one
srishti-negi Jun 1, 2026
7ca810b
feat(telemetry): Integrate OneDrive Sync Client active users report a…
srishti-negi Jun 1, 2026
d5bcd86
Merge pull request #24 from srishti-negi/onedrive-sync-telemetry
srishti-negi Jun 1, 2026
cd118a3
feat(security): Integrate Data Security & Governance sensitivity labe…
srishti-negi Jun 1, 2026
a86796a
style(security): Enable dynamic auto-wrap on resizing in sensitivity …
srishti-negi Jun 1, 2026
17cd719
Merge pull request #25 from srishti-negi/data-security-governance
srishti-negi Jun 1, 2026
820af58
feat(telemetry): Integrate OneNote active users telemetry row inside …
srishti-negi Jun 1, 2026
415a0fe
Merge pull request #26 from srishti-negi/onenote-users-telemetry
srishti-negi Jun 1, 2026
2937144
Implement PowerAutomateScanner for tenant-wide flow discovery and tel…
NityamPareek Jun 2, 2026
0de3ce5
Add matplotlib for the pip installs needed
projjal1611 Jun 2, 2026
5b51b45
feat: integrate separated OneDrive/SharePoint storage and Data Securi…
srishti-negi Jun 2, 2026
5581f36
Merge pull request #28 from srishti-negi/splash-one
srishti-negi Jun 2, 2026
8e93064
Add new UI layout
projjal1611 Jun 2, 2026
08ddf8a
Lazily Fetch the reports on demand
projjal1611 Jun 2, 2026
0619f5c
feat(security): add Protection and Mode columns to Sensitivity Label …
srishti-negi Jun 2, 2026
978f80a
Merge pull request #30 from srishti-negi/security-metadata-columns
srishti-negi Jun 3, 2026
3d47996
Implement unified telemetry logging without stdout
projjal1611 Jun 3, 2026
4e64a23
Merge branch 'origin/splash-one' into splash-one
projjal1611 Jun 3, 2026
b3d724a
Sanitize and mask access tokens and credentials from logs
projjal1611 Jun 3, 2026
ac33289
Fix NameError: name 'logger' is not defined in reports.py
projjal1611 Jun 3, 2026
4a823ba
Implement certificate-based hybrid authentication flow and UI setup i…
NityamPareek Jun 3, 2026
0f1e8da
Merge remote-tracking branch 'origin/splash-one' into splash-one
NityamPareek Jun 3, 2026
73cf63c
Add PowerShellClient and RetentionService for Retention Policy scanni…
srishti-negi Jun 3, 2026
0fbcbf9
Enhance UI components styling to modern Material Design 3
projjal1611 Jun 3, 2026
5e12d08
Merge branch 'origin/splash-one' into splash-one
projjal1611 Jun 3, 2026
b914b66
Wrap dashboard sections in card layout, integrate dynamic height slid…
projjal1611 Jun 3, 2026
ce5bc68
Implement Exchange Online Mailbox Usage Telemetry UI and API integration
NityamPareek Jun 3, 2026
5ad85c7
Rename connection page titles and labels to Deal Assistant
projjal1611 Jun 3, 2026
f6e3adf
Update sidebar brand heading to Deal Assistant and change icon to han…
projjal1611 Jun 3, 2026
990168d
Style Exchange Online Mailbox Usage Telemetry section as card component
NityamPareek Jun 3, 2026
d283dc0
Merge branch 'origin/splash-one' into splash-one
projjal1611 Jun 3, 2026
7235404
Detailed CSV exports and performance optimisations for Security telem…
srishti-negi Jun 3, 2026
0b793d8
fix(security): resolve retention policy duration showing N/A due to D…
srishti-negi Jun 3, 2026
c761746
feat(flet): implement matplotlib chart and individual card retries
projjal1611 Jun 3, 2026
549159d
Merge branch 'origin/splash-one' into splash-one
projjal1611 Jun 3, 2026
c273a06
docs: add cryptography to dependency installation commands in README
projjal1611 Jun 4, 2026
6690048
feat: remove Workforce analysis and Cost savings plan from telemetry …
projjal1611 Jun 4, 2026
ee1fdf4
feat: isolate certificates & reports per tenant_client, clean up tele…
NityamPareek Jun 4, 2026
7276868
feat: add external service plan reference links to SKU inventory summ…
projjal1611 Jun 4, 2026
74acf79
feat(telemetry): add shared mailbox and public folder stats telemetry…
NityamPareek Jun 4, 2026
78b01a2
refactor(telemetry): replace csv library with memory-efficient pandas…
NityamPareek Jun 4, 2026
5b12709
fix(telemetry): dynamically check CSV headers to handle missing colum…
NityamPareek Jun 4, 2026
cee14de
feat: add dynamic log directory switching per tenant and client with …
projjal1611 Jun 4, 2026
1941282
feat(security): add static eDiscovery Cases guidance card
srishti-negi Jun 4, 2026
b987a8e
feat(telemetry): add height scale slider and reduce font sizes for Po…
srishti-negi Jun 4, 2026
f34c7b5
fix(security): pass correct cert_tenant_id Guid to PowerShellClient i…
srishti-negi Jun 4, 2026
3c2ba2b
local: implement dynamic logging segregation and refactor telemetry c…
projjal1611 Jun 4, 2026
3145d98
merge: merge origin/splash-one into local branch, resolving delete/mo…
projjal1611 Jun 4, 2026
dd13b59
feat(telemetry): apply remote height slider and font size adjustments…
projjal1611 Jun 4, 2026
414a027
Optimize in-memory footprint of telemetry modules with streaming CSV …
projjal1611 Jun 4, 2026
42d8ec6
Rename license_log.txt to telemetry_log.txt to reflect entire app log…
projjal1611 Jun 4, 2026
2b60835
Implement concurrency control using BoundedSemaphore to limit active …
projjal1611 Jun 4, 2026
7400684
Increase telemetry parallel execution limit from 3 to 6 reports
projjal1611 Jun 4, 2026
65e5f01
Implement Exchange Online Calendar Telemetry using PowerShell metadat…
NityamPareek Jun 5, 2026
affc9b6
Merge branch 'origin/splash-one' into splash-one and resolve conflict…
projjal1611 Jun 6, 2026
ba3afa2
Clear/reset telemetry coordinator tab and hide all sub-views/grids wh…
projjal1611 Jun 6, 2026
813b4be
Merge origin/main into splash-one branch, resolving conflicts in READ…
projjal1611 Jun 6, 2026
dacbe4a
Restore main branch versions of estimators/factory.py, util/auth_mana…
projjal1611 Jun 6, 2026
2525501
Update .gitignore to ignore generated application artifacts and macOS…
projjal1611 Jun 6, 2026
4d934e8
Rename migration_planner_telemetry.py to deal_assistant.py and update…
projjal1611 Jun 6, 2026
dd73d59
Merge Migration Planner to Deal Assistant
projjal1611 Jun 6, 2026
d23b016
feat(telemetry): add option to download M365 usage report as PDF
projjal1611 Jun 6, 2026
71afff0
refactor: migrate matplotlib to backend-agnostic Figure objects and p…
projjal1611 Jun 6, 2026
7ec848e
clean: remove scratch files debug_hang.py and test_output.pdf
projjal1611 Jun 6, 2026
ca4557b
Update certificate upload instructions to be optional, with MD3 styli…
projjal1611 Jun 8, 2026
5a20f66
Refactor Exchange Online and Files sections into unified parent cards…
projjal1611 Jun 8, 2026
2101b27
Optimize memory footprint by processing CSV reads and writes in chunks
projjal1611 Jun 8, 2026
0c217f7
Memory optimization: SQLite UI pagination, disk streaming, and lazy g…
projjal1611 Jun 8, 2026
6bc23fa
feat(telemetry): Add Directory section with Domains list and Groups c…
projjal1611 Jun 8, 2026
c85087b
feat(telemetry): Add user counts and rename groups section to Groups …
projjal1611 Jun 8, 2026
d1e4630
feat(security): implement Entra Conditional Access API querying and s…
srishti-negi Jun 8, 2026
f195201
refactor(telemetry): Rename Directory section to Directory Summary
projjal1611 Jun 8, 2026
56210ec
style(security): upgrade Authentication Mechanics card to 3 standalon…
srishti-negi Jun 8, 2026
8774c9e
style(security): update Authentication Mechanics card to render as a …
srishti-negi Jun 8, 2026
9f6e76a
style(security): refine Authentication Mechanics table to render exac…
srishti-negi Jun 8, 2026
eaf9120
Implement Entra Conditional Access API querying and standalone Authen…
srishti-negi Jun 8, 2026
c616dd6
feat(telemetry): Club active users and app usage under App Usage Summary
projjal1611 Jun 8, 2026
9262a83
feat(security): integrate Entra Service Principals preferredSingleSig…
srishti-negi Jun 8, 2026
c717fa5
feat(telemetry): Add mail public folders count and component-level er…
projjal1611 Jun 8, 2026
8593dec
Merge pull request #47 from srishti-negi/saml-sso-telemetry
srishti-negi Jun 8, 2026
7a47ebd
feat(telemetry): Add Integrated Apps section displaying organization-…
projjal1611 Jun 8, 2026
8f4bb75
feat(telemetry): audit browser-based (OWA) and non-browser (Desktop/M…
srishti-negi Jun 9, 2026
4f4d29f
Merge pull request #48 from srishti-negi/email-client-support
srishti-negi Jun 9, 2026
cf491d1
feat(telemetry): audit Discovered PST Files (Cloud/Endpoints) under u…
srishti-negi Jun 9, 2026
2dd1b1b
Merge pull request #49 from srishti-negi/pst-email-environment
srishti-negi Jun 9, 2026
f7618cb
feat(telemetry): implement unified Email Environment dashboard card a…
srishti-negi Jun 9, 2026
1d222f5
merge: resolve conflicts and sync upstream/splash-one into local spla…
srishti-negi Jun 9, 2026
7d409c5
feat(telemetry): implement unified Email Environment dashboard card a…
srishti-negi Jun 9, 2026
a67ba73
Merge pull request #50 from srishti-negi/current-email-systems
srishti-negi Jun 9, 2026
e9c7610
feat(telemetry): treat Undetermined client values as active usage in …
srishti-negi Jun 9, 2026
fd3177f
Merge pull request #51 from srishti-negi/undetermined-telemetry-activity
srishti-negi Jun 9, 2026
d030c69
Use aggregated EmailAppUsageAppsUserCounts report for email environme…
projjal1611 Jun 9, 2026
7eedf1d
feat(telemetry): remove Operational Reality column and implement dyna…
srishti-negi Jun 9, 2026
eb454c1
Merge remote-tracking branch 'origin/splash-one' into remove-operatio…
srishti-negi Jun 9, 2026
f41d38b
Merge pull request #52 from srishti-negi/remove-operational-reality-col
srishti-negi Jun 9, 2026
06679a2
local work: m365 apps consolidation, pagination, disclaimer
projjal1611 Jun 10, 2026
428d148
feat: Introduce User Persona Analysis sidebar view and input screen
NityamPareek Jun 10, 2026
c59dd99
feat: Add user persona dataset generation pipeline using SQLite for O…
NityamPareek Jun 10, 2026
4c6b7dc
refactor: Move Exchange Connectors UI to Exchange Online section
srishti-negi Jun 10, 2026
e176ee7
feat: Add backend PowerShell script and Python wrapper for Exchange C…
srishti-negi Jun 10, 2026
8d7e6d7
feat: Connect user persona dataset generation pipeline to UI, with re…
NityamPareek Jun 10, 2026
8b412be
fix(ui): inject missing header and align grid columns in EmailClientS…
srishti-negi Jun 10, 2026
104bb20
fix(ui): update headers for Email Client Support and Exchange Online …
srishti-negi Jun 10, 2026
74be482
fix(ui): decouple email client loading indicators to render headings …
srishti-negi Jun 10, 2026
7d7ce34
Merge pull request #55 from srishti-negi/refactor-connectors-splash-one
srishti-negi Jun 10, 2026
450c8fb
feat: Refine persona classification for idle accounts, add experiment…
NityamPareek Jun 10, 2026
762a9ae
feat: implement thread-safe fetch cancellation and stale thread filte…
projjal1611 Jun 11, 2026
25762b0
feat: enable partial PDF report generation after cancellation or fail…
projjal1611 Jun 11, 2026
75c06b7
feat: Add precise, centered execution timers next to all section and …
projjal1611 Jun 11, 2026
8b8176a
fix: Robustly enable PDF downloads for partial reports and prevent At…
projjal1611 Jun 11, 2026
e5abb4a
feat: Add K-Means pre-clustering strategy with dynamic Silhouette opt…
NityamPareek Jun 11, 2026
8428a74
feat(telemetry): Add Intune Device Configurations UI module
srishti-negi Jun 12, 2026
75d7e88
feat(telemetry): add Mail Security SKUs tracker for EOP and Defender
srishti-negi Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,15 @@ __pycache__/
env/
outputs/
data/
test_*.py
test_*.py

# Generated application artifacts
certificate/
certificates/
telemetry/reports/
telemetry/logs/

# macOS Finder metadata
.DS_Store
.DS_Store?
._*
188 changes: 188 additions & 0 deletions core/cert_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Certificate helper management functions for local authentication mapping to Microsoft Graph API."""

import os
import logging
import datetime
from typing import Tuple
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import pkcs12

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

def setup_logging() -> None:
"""Configures file logging to telemetry/logs/certificate_auth_log.txt."""
if any(isinstance(h, logging.FileHandler) for h in logger.handlers):
return

root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
log_dir = os.path.join(root, "telemetry", "logs")
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, "certificate_auth_log.txt")

file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.propagate = True

setup_logging()


def update_log_directory(tenant_id: str = None, client_id: str = None) -> None:
"""Updates the log directory dynamically once tenant and client ID are known, or reverts to default."""
# Find existing FileHandlers on the logger and remove them
for h in list(logger.handlers):
if isinstance(h, logging.FileHandler):
h.close()
logger.removeHandler(h)

root = get_project_root()
if tenant_id and client_id:
log_dir = os.path.join(root, "telemetry", "logs", f"{tenant_id}_{client_id}")
else:
log_dir = os.path.join(root, "telemetry", "logs")

os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, "certificate_auth_log.txt")

file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)

logger.addHandler(file_handler)


def get_project_root() -> str:
"""Helper to locate the root path of migration-planner."""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def get_cert_paths(cert_dir: str = "certificate", tenant_id: str = None, client_id: str = None) -> Tuple[str, str, str]:
"""Returns absolute paths for cert directory, certificate.pem, and passkey.pfx."""
root = get_project_root()
if tenant_id and client_id:
dir_path = os.path.join(root, cert_dir, f"{tenant_id}_{client_id}")
else:
dir_path = os.path.join(root, cert_dir)
pem_path = os.path.join(dir_path, "certificate.pem")
pfx_path = os.path.join(dir_path, "passkey.pfx")
return dir_path, pem_path, pfx_path

def check_certificate_exists(cert_dir: str = "certificate", tenant_id: str = None, client_id: str = None) -> bool:
"""Checks if the certificate directory exists and contains a valid cert.pfx file."""
_, _, pfx_path = get_cert_paths(cert_dir, tenant_id, client_id)
exists = os.path.exists(pfx_path)
logger.info("Checking if certificate exists at %s: %s", pfx_path, exists)
return exists

def generate_certificate(client_secret: str, cert_dir: str = "certificate", common_name: str = "LocalAppHybridAuth", tenant_id: str = None, client_id: str = None) -> Tuple[str, str]:
"""Generates a self-signed certificate and PFX bundle using client_secret as the password.

Returns:
Tuple containing absolute paths to the generated cert.pem and cert.pfx files.
"""
logger.info("Initializing certificate generation flow...")
dir_path, pem_path, pfx_path = get_cert_paths(cert_dir, tenant_id, client_id)

os.makedirs(dir_path, exist_ok=True)
logger.info("Certificate directory confirmed: %s", dir_path)

secret_bytes = client_secret.encode('utf-8')

logger.info("Generating secure RSA private key...")
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)

logger.info("Generating self-signed certificate with CN: %s...", common_name)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
])

cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.now(datetime.timezone.utc)
).not_valid_after(
# Valid for 2 years
datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=730)
).sign(private_key, hashes.SHA256())

logger.info("Writing PEM public certificate to %s...", pem_path)
with open(pem_path, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))

logger.info("Serializing and writing PFX encrypted with client secret to %s...", pfx_path)
pfx_bytes = pkcs12.serialize_key_and_certificates(
name=common_name.encode('utf-8'),
key=private_key,
cert=cert,
cas=None,
encryption_algorithm=serialization.BestAvailableEncryption(secret_bytes)
)
with open(pfx_path, "wb") as f:
f.write(pfx_bytes)

logger.info("Certificate generation complete! Files written successfully.")
return pem_path, pfx_path

def load_certificate(client_secret: str, cert_dir: str = "certificate", tenant_id: str = None, client_id: str = None) -> Tuple[str, str]:
"""Decrypts PFX bundle using client_secret, extracting private key PEM and SHA1 thumbprint.

Returns:
Tuple containing private_key_pem (str) and thumbprint (str).
"""
logger.info("Loading certificate files...")
_, _, pfx_path = get_cert_paths(cert_dir, tenant_id, client_id)

if not os.path.exists(pfx_path):
raise FileNotFoundError(f"PFX certificate file not found at {pfx_path}")

logger.info("Unlocking PFX file with client secret...")
with open(pfx_path, "rb") as f:
pfx_data = f.read()

password_bytes = client_secret.encode('utf-8')
private_key, certificate, _ = pkcs12.load_key_and_certificates(
pfx_data,
password_bytes
)

logger.info("Extracting private key PEM...")
private_key_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
).decode("utf-8")

logger.info("Calculating certificate SHA1 fingerprint...")
thumbprint = certificate.fingerprint(hashes.SHA1()).hex()
logger.info("Loaded certificate successfully. Thumbprint: %s", thumbprint)

return private_key_pem, thumbprint
15 changes: 15 additions & 0 deletions core/graph/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unified Microsoft Graph API service layer."""
84 changes: 84 additions & 0 deletions core/graph/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unified client managing connection pools, authentication, and sessions for Microsoft Graph."""

import logging
from typing import List, Dict, Any, Union, Optional
import requests
from util.auth_manager import TokenManager
from util.connectors import UrlInvoker

logger = logging.getLogger(__name__)

class GraphClient:
"""Unified client managing credentials validation and connection slots to Microsoft Graph."""

def __init__(
self,
tenant_id: str,
client_ids: Union[str, List[str]],
client_secrets: Union[str, List[str]],
concurrency: int = 1,
retries: int = 5,
backoff: int = 2
) -> None:
self.tenant_id = tenant_id

# Normalize inputs to lists to perfectly match TokenManager parameters
self.client_ids = [client_ids] if isinstance(client_ids, str) else client_ids
self.client_secrets = [client_secrets] if isinstance(client_secrets, str) else client_secrets

self.token_manager = TokenManager(
tenant_id=self.tenant_id,
client_ids=self.client_ids,
client_secrets=self.client_secrets,
concurrency=concurrency,
retries=retries,
backoff=backoff
)

self.url_invoker = UrlInvoker(
token_manager=self.token_manager,
batch_retry_count=retries,
batch_backoff=backoff,
initial_delay=1,
jitter=0.5
)

def authenticate(self, required_scopes: Optional[List[str]] = None) -> None:
"""Validates Entra ID scopes and fetches active tokens."""
self.token_manager.authenticate_all(required_scopes=required_scopes)

def get_session(self) -> requests.Session:
"""Returns the unified requests Session pool."""
return self.token_manager.get_session()

def get_active_token(self) -> Dict[str, Any]:
"""Acquires an active, refreshed token slot from TokenManager lease queue."""
return self.token_manager.get_valid_token_slot()

def release_token(self, token_slot: Dict[str, Any]) -> None:
"""Returns token slot to the queue, completing the lease cycle."""
self.token_manager.return_token_slot(token_slot)

def close(self) -> None:
"""Releases all connection pool and socket resources."""
self.token_manager.close()

def __enter__(self) -> "GraphClient":
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.close()
Loading