Erster Checkin: Tool arbeitet
This commit is contained in:
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Package-Initialisierung für m365-workingwith
|
||||||
28
app/aggregator.py
Normal file
28
app/aggregator.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from collections import Counter
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
from .models import WorkingWithRelation
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DepartmentLink:
|
||||||
|
source_department: str
|
||||||
|
destination_department: str
|
||||||
|
weight: int
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_department_links(relations: List[WorkingWithRelation]) -> List[DepartmentLink]:
|
||||||
|
counter: Counter[tuple[str, str]] = Counter()
|
||||||
|
for rel in relations:
|
||||||
|
src = (rel.source.department or "").strip()
|
||||||
|
dst = (rel.destination.department or "").strip()
|
||||||
|
if not src or not dst:
|
||||||
|
continue
|
||||||
|
counter[(src, dst)] += 1
|
||||||
|
|
||||||
|
links: List[DepartmentLink] = [
|
||||||
|
DepartmentLink(source_department=s, destination_department=d, weight=w)
|
||||||
|
for (s, d), w in counter.items()
|
||||||
|
]
|
||||||
|
return sorted(links, key=lambda l: l.weight, reverse=True)
|
||||||
51
app/auth.py
Normal file
51
app/auth.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
AUTH_STATE_FILE = Path("auth_state.json")
|
||||||
|
|
||||||
|
|
||||||
|
async def ensure_login(email: str) -> None:
|
||||||
|
"""
|
||||||
|
Öffnet den Browser mit/ohne bestehenden auth_state.
|
||||||
|
Beim ersten Mal: manueller Login in M365.
|
||||||
|
Danach wird der Auth-State persistiert.
|
||||||
|
"""
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser_type = p.chromium
|
||||||
|
|
||||||
|
if AUTH_STATE_FILE.exists():
|
||||||
|
context = await browser_type.launch_persistent_context(
|
||||||
|
user_data_dir="user_data",
|
||||||
|
headless=False,
|
||||||
|
channel="chrome",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
context = await browser_type.launch_persistent_context(
|
||||||
|
user_data_dir="user_data",
|
||||||
|
headless=False,
|
||||||
|
channel="chrome",
|
||||||
|
)
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
await page.goto("https://m365.cloud.microsoft/search/?auth=2")
|
||||||
|
|
||||||
|
print(f"Bitte mit {email} in Microsoft 365 anmelden.")
|
||||||
|
print("Nach erfolgreichem Login Browser-Fenster schliessen oder Skript abbrechen (Ctrl+C).")
|
||||||
|
|
||||||
|
# Langes Warten; in echter Implementierung könnte man Events verwenden
|
||||||
|
await page.wait_for_load_state("domcontentloaded")
|
||||||
|
'page.wait_for_timeout(1000 * 1000)'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
state = await context.storage_state()
|
||||||
|
# storage_state() liefert dict -> JSON speichern
|
||||||
|
AUTH_STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
|
||||||
|
|
||||||
|
await context.close()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_login_sync(email: str) -> None:
|
||||||
|
asyncio.run(ensure_login(email))
|
||||||
101
app/easyvisualize.py
Normal file
101
app/easyvisualize.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# --- KONFIGURATION & DATEIPFAD ---
|
||||||
|
|
||||||
|
# Prüfen, ob ein Pfad als Argument übergeben wurde, sonst Default nutzen
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
csv_dateipfad = sys.argv[1]
|
||||||
|
else:
|
||||||
|
csv_dateipfad = '/output/relations.csv'
|
||||||
|
|
||||||
|
# Sicherstellen, dass die Datei existiert
|
||||||
|
|
||||||
|
if not os.path.exists(csv_dateipfad):
|
||||||
|
print(f"Fehler: Die Datei '{csv_dateipfad}'wurde nicht gefunden.")
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --- DATEN LADEN ---
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Wir laden die CSV (Trennzeichen ; ist im deutschen Excel-Raum Standard)
|
||||||
|
df =pd.read_csv(csv_dateipfad, sep=None, engine='python')
|
||||||
|
print(f"Daten erfolgreich geladen: {len(df)} Zeile aus '{csv_dateipfad}'.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Lesen der CSV: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# --- DATEN AGGREGIEREN ---
|
||||||
|
# Wir zählen die Kommunikationspfade zwischen den Abteilungen
|
||||||
|
|
||||||
|
''' df_agg = df.groupby(['source_department', 'destination_department']).size().reset_index(name='weight') '''
|
||||||
|
df_agg = df.groupby(['source_department', 'destination_displayname']).size().reset_index(name='weight')
|
||||||
|
|
||||||
|
# Liste aller Departments für die Knoten-Beschriftung
|
||||||
|
''' all_nodes = list(pd.concat([df_agg['source_department'], df_agg['destination_department']]).unique()) '''
|
||||||
|
all_nodes = list(pd.concat([df_agg['source_department'], df_agg['destination_displayname']]).unique())
|
||||||
|
|
||||||
|
node_map = {name: i for i, name in enumerate(all_nodes)}
|
||||||
|
|
||||||
|
# Mapping auf Indizes
|
||||||
|
source_indices = df_agg['source_department'].map(node_map)
|
||||||
|
target_indices = df_agg['destination_displayname'].map(node_map)
|
||||||
|
''' target_indices = df_agg['destination_department'].map(node_map) '''
|
||||||
|
|
||||||
|
weights = df_agg['weight']
|
||||||
|
|
||||||
|
|
||||||
|
# --- FARBGESTALTUNG ---
|
||||||
|
# Wir färben die "Source"-Abteilungen (dein Team) anders ein als die "Ziele"
|
||||||
|
node_colors = ["#1f77b4"
|
||||||
|
if node in df['source_department'].unique() else "#9467bd" for node in all_nodes]
|
||||||
|
|
||||||
|
# --- VISUALISIERUNG ---
|
||||||
|
fig = go.Figure(data=[go.Sankey(
|
||||||
|
node=dict(
|
||||||
|
pad=20,
|
||||||
|
thickness=30,
|
||||||
|
line=dict(color="black", width=0.5),
|
||||||
|
label=all_nodes,
|
||||||
|
color=node_colors
|
||||||
|
),
|
||||||
|
|
||||||
|
link=dict(
|
||||||
|
source=source_indices,
|
||||||
|
target=target_indices,
|
||||||
|
value=weights,
|
||||||
|
color="rgba(200, 200, 200, 0.5)"
|
||||||
|
# Transparente graue Pfade
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
title_text=f"Organisatorische Netzwerkanalyse: Abteilungs-Flüsse<br><sup>Quelle: {csv_dateipfad}</sup>",
|
||||||
|
font_size=12,
|
||||||
|
height=800
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- AUSGABE ---
|
||||||
|
output_filename = "netzwerk_analyse_lokal.html"
|
||||||
|
|
||||||
|
fig.write_html(output_filename)
|
||||||
|
|
||||||
|
print(f"Analyse abgeschlossen. Interaktives Diagramm gespeichert unter: {output_filename}")
|
||||||
|
|
||||||
|
# Automatisches Öffnen im Standardbrowser
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
webbrowser.open('file://'+ os.path.realpath(output_filename))
|
||||||
|
|
||||||
174
app/extractor.py
Normal file
174
app/extractor.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from playwright.async_api import async_playwright, Page, Response
|
||||||
|
|
||||||
|
from .auth import AUTH_STATE_FILE
|
||||||
|
from .parser import (
|
||||||
|
parse_person_from_organization_person,
|
||||||
|
parse_direct_emails_from_organization,
|
||||||
|
parse_workingwith_entries,
|
||||||
|
)
|
||||||
|
from .models import Person, WorkingWithRelation
|
||||||
|
|
||||||
|
|
||||||
|
DELV_PERSON_URL = "https://eur.loki.delve.office.com/api/v2/person"
|
||||||
|
DELV_ORG_URL = "https://eur.loki.delve.office.com/api/v1/organization"
|
||||||
|
DELV_WORKINGWITH_URL = "https://eur.loki.delve.office.com/api/v1/workingwith"
|
||||||
|
|
||||||
|
|
||||||
|
async def _collect_json_from_responses(page: Page) -> Dict[str, Any]:
|
||||||
|
collected: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
async def handle_response(response: Response):
|
||||||
|
url = response.url
|
||||||
|
try:
|
||||||
|
if url.startswith(DELV_PERSON_URL):
|
||||||
|
collected["person"] = await response.json()
|
||||||
|
elif url.startswith(DELV_ORG_URL):
|
||||||
|
collected["organization"] = await response.json()
|
||||||
|
elif url.startswith(DELV_WORKINGWITH_URL):
|
||||||
|
collected["workingwith"] = await response.json()
|
||||||
|
except Exception:
|
||||||
|
# JSON-Parsing-Fehler ignorieren
|
||||||
|
pass
|
||||||
|
|
||||||
|
page.on("response", handle_response)
|
||||||
|
return collected
|
||||||
|
|
||||||
|
|
||||||
|
async def _open_profile_and_collect(page: Page, email: str) -> Dict[str, Any]:
|
||||||
|
collected = await _collect_json_from_responses(page)
|
||||||
|
|
||||||
|
await page.goto("https://m365.cloud.microsoft/search/?auth=2")
|
||||||
|
|
||||||
|
# Suche öffnen
|
||||||
|
search_button = page.get_by_role("button", name="search")
|
||||||
|
await search_button.click()
|
||||||
|
|
||||||
|
input_box = page.locator('input[type="text"]')
|
||||||
|
await input_box.fill(f'Person:"{email}"')
|
||||||
|
await input_box.press("Enter")
|
||||||
|
|
||||||
|
# Warten bis ein Profil-Button erscheint
|
||||||
|
await page.wait_for_timeout(2000)
|
||||||
|
profile_button = page.get_by_title("Organisation")
|
||||||
|
'await profile_button.click()'
|
||||||
|
|
||||||
|
# Organisation-Tab
|
||||||
|
org_button = page.locator('button[data-content="Organisation"]')
|
||||||
|
await org_button.click()
|
||||||
|
|
||||||
|
# Zeit geben, Netzwerk-Calls zu sammeln
|
||||||
|
await page.wait_for_timeout(4000)
|
||||||
|
return collected
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_relations_for_manager(manager_email: str) -> List[WorkingWithRelation]:
|
||||||
|
if not AUTH_STATE_FILE.exists():
|
||||||
|
raise RuntimeError("auth_state.json nicht vorhanden, bitte zuerst login-Modus ausführen.")
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser_type = p.chromium
|
||||||
|
context = await browser_type.launch_persistent_context(
|
||||||
|
user_data_dir="user_data",
|
||||||
|
headless=False,
|
||||||
|
channel="chrome"
|
||||||
|
)
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
collected = await _open_profile_and_collect(page, manager_email)
|
||||||
|
|
||||||
|
person_json = collected.get("person") or {}
|
||||||
|
org_json = collected.get("organization") or {}
|
||||||
|
working_json = collected.get("workingwith") or {}
|
||||||
|
|
||||||
|
manager_person = parse_person_from_organization_person(person_json)
|
||||||
|
print(manager_person)
|
||||||
|
working_with_persons = parse_workingwith_entries(working_json)
|
||||||
|
print(working_with_persons)
|
||||||
|
directs_emails = parse_direct_emails_from_organization(org_json)
|
||||||
|
print(directs_emails)
|
||||||
|
|
||||||
|
relations: List[WorkingWithRelation] = []
|
||||||
|
|
||||||
|
# Manager -> WorkingWith
|
||||||
|
for dest in working_with_persons:
|
||||||
|
relations.append(WorkingWithRelation(source=manager_person, destination=dest))
|
||||||
|
|
||||||
|
# Für alle Directs ebenfalls WorkingWith holen
|
||||||
|
for direct_email in directs_emails:
|
||||||
|
collected_direct = await _open_profile_and_collect(page, direct_email)
|
||||||
|
person_json_d = collected_direct.get("person") or {}
|
||||||
|
working_json_d = collected_direct.get("workingwith") or {}
|
||||||
|
|
||||||
|
direct_person = parse_person_from_organization_person(person_json_d)
|
||||||
|
working_with_persons_d = parse_workingwith_entries(working_json_d)
|
||||||
|
|
||||||
|
for dest in working_with_persons_d:
|
||||||
|
relations.append(WorkingWithRelation(source=direct_person, destination=dest))
|
||||||
|
|
||||||
|
await context.close()
|
||||||
|
return relations
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_relations_for_emails(emails: List[str]) -> List[WorkingWithRelation]:
|
||||||
|
if not AUTH_STATE_FILE.exists():
|
||||||
|
raise RuntimeError("auth_state.json nicht vorhanden – bitte zuerst login-Modus ausführen.")
|
||||||
|
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser_type = p.chromium
|
||||||
|
context = await browser_type.launch_persistent_context(
|
||||||
|
user_data_dir="user_data",
|
||||||
|
headless=False,
|
||||||
|
channel="chrome"
|
||||||
|
)
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
relations: List[WorkingWithRelation] = []
|
||||||
|
|
||||||
|
for email in emails:
|
||||||
|
collected = await _open_profile_and_collect(page, email)
|
||||||
|
person_json = collected.get("person") or {}
|
||||||
|
working_json = collected.get("workingwith") or {}
|
||||||
|
|
||||||
|
person = parse_person_from_organization_person(person_json)
|
||||||
|
working_with_persons = parse_workingwith_entries(working_json)
|
||||||
|
|
||||||
|
for dest in working_with_persons:
|
||||||
|
relations.append(WorkingWithRelation(source=person, destination=dest))
|
||||||
|
|
||||||
|
await context.close()
|
||||||
|
return relations
|
||||||
|
|
||||||
|
|
||||||
|
def write_relations_to_csv(relations: List[WorkingWithRelation], output_path: Path) -> None:
|
||||||
|
fieldnames = [
|
||||||
|
"source_mail",
|
||||||
|
"source_displayname",
|
||||||
|
"source_jobTitle",
|
||||||
|
"source_department",
|
||||||
|
"destination_mail",
|
||||||
|
"destination_displayname",
|
||||||
|
"destination_jobTitle",
|
||||||
|
"destination_department",
|
||||||
|
]
|
||||||
|
with output_path.open("w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
for rel in relations:
|
||||||
|
writer.writerow(
|
||||||
|
{
|
||||||
|
"source_mail": rel.source.email,
|
||||||
|
"source_displayname": rel.source.display_name,
|
||||||
|
"source_jobTitle": rel.source.job_title,
|
||||||
|
"source_department": rel.source.department,
|
||||||
|
"destination_mail": rel.destination.email,
|
||||||
|
"destination_displayname": rel.destination.display_name,
|
||||||
|
"destination_jobTitle": rel.destination.job_title,
|
||||||
|
"destination_department": rel.destination.department,
|
||||||
|
}
|
||||||
|
)
|
||||||
60
app/main.py
Normal file
60
app/main.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .auth import ensure_login_sync
|
||||||
|
from .extractor import (
|
||||||
|
extract_relations_for_manager,
|
||||||
|
extract_relations_for_emails,
|
||||||
|
write_relations_to_csv,
|
||||||
|
)
|
||||||
|
from .aggregator import aggregate_department_links
|
||||||
|
|
||||||
|
|
||||||
|
OUTPUT_DIR = Path("output")
|
||||||
|
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> None:
|
||||||
|
if len(argv) < 2:
|
||||||
|
print("Verwendung:")
|
||||||
|
print(" python -m app.main login <email>")
|
||||||
|
print(" python -m app.main manager <manager_email>")
|
||||||
|
print(" python -m app.main emails <email1> [<email2> ...]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
mode = argv[1].lower()
|
||||||
|
|
||||||
|
if mode == "login":
|
||||||
|
if len(argv) != 3:
|
||||||
|
print("login-Modus benötigt genau 1 E-Mail-Adresse.")
|
||||||
|
sys.exit(1)
|
||||||
|
email = argv[2]
|
||||||
|
ensure_login_sync(email)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif mode == "manager":
|
||||||
|
if len(argv) != 3:
|
||||||
|
print("manager-Modus benötigt genau 1 Manager-E-Mail.")
|
||||||
|
sys.exit(1)
|
||||||
|
manager_email = argv[2]
|
||||||
|
relations = asyncio.run(extract_relations_for_manager(manager_email))
|
||||||
|
|
||||||
|
elif mode == "emails":
|
||||||
|
if len(argv) < 3:
|
||||||
|
print("emails-Modus benötigt mindestens 1 E-Mail-Adresse.")
|
||||||
|
sys.exit(1)
|
||||||
|
emails = argv[2:]
|
||||||
|
relations = asyncio.run(extract_relations_for_emails(emails))
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Unbekannter Modus: {mode}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
csv_path = OUTPUT_DIR / "relations.csv"
|
||||||
|
write_relations_to_csv(relations, csv_path)
|
||||||
|
print(f"CSV geschrieben: {csv_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv)
|
||||||
16
app/models.py
Normal file
16
app/models.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Person:
|
||||||
|
email: str
|
||||||
|
display_name: Optional[str]
|
||||||
|
job_title: Optional[str]
|
||||||
|
department: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkingWithRelation:
|
||||||
|
source: Person
|
||||||
|
destination: Person
|
||||||
131
app/parser.py
Normal file
131
app/parser.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from .models import Person
|
||||||
|
|
||||||
|
def _first_or_none(items: Optional[list]) -> Optional[dict]:
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
return items[0] or None
|
||||||
|
|
||||||
|
def parse_person_from_organization_person(json_obj: Dict[str, Any]) -> Person:
|
||||||
|
"""
|
||||||
|
Erwartet ein Objekt der Form:
|
||||||
|
|
||||||
|
{
|
||||||
|
"person": {
|
||||||
|
"names": [
|
||||||
|
{
|
||||||
|
"value": {
|
||||||
|
"displayName": "Display, Name-MGB",
|
||||||
|
"givenName": "Name",
|
||||||
|
"surname": "Display"
|
||||||
|
},
|
||||||
|
"source": "Organisation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emailAddresses": [
|
||||||
|
{
|
||||||
|
"value": {
|
||||||
|
"name": "display.name@mgb.ch",
|
||||||
|
"address": "display.name@mgb.ch"
|
||||||
|
},
|
||||||
|
"source": "Organisation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"workDetails": [
|
||||||
|
{
|
||||||
|
"value": {
|
||||||
|
"companyName": "Migros-Genossenschafts-Bund",
|
||||||
|
"jobTitle": "Title",
|
||||||
|
"department": "Dept",
|
||||||
|
"office": "HH-xx"
|
||||||
|
},
|
||||||
|
"source": "Organisation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
person_data = json_obj.get("person", {})
|
||||||
|
|
||||||
|
# Namen
|
||||||
|
names_entry = _first_or_none(person_data.get("names"))
|
||||||
|
names_value = (names_entry or {}).get("value", {}) if names_entry else {}
|
||||||
|
display_name = names_value.get("displayName")
|
||||||
|
|
||||||
|
# E-Mail
|
||||||
|
mail_entry = _first_or_none(person_data.get("emailAddresses"))
|
||||||
|
mail_value = (mail_entry or {}).get("value", {}) if mail_entry else {}
|
||||||
|
email = mail_value.get("address")
|
||||||
|
|
||||||
|
# WorkDetails: Firma, Job, Abteilung, Büro
|
||||||
|
work_entry = _first_or_none(person_data.get("workDetails"))
|
||||||
|
work_value = (work_entry or {}).get("value", {}) if work_entry else {}
|
||||||
|
company_name = work_value.get("companyName")
|
||||||
|
job_title = work_value.get("jobTitle")
|
||||||
|
department = work_value.get("department")
|
||||||
|
office = work_value.get("office")
|
||||||
|
|
||||||
|
return Person(
|
||||||
|
email=email,
|
||||||
|
display_name=display_name,
|
||||||
|
job_title=job_title,
|
||||||
|
department=department,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_direct_emails_from_organization(json_obj: dict[str, Any]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Erwartete Struktur (aus organization_anonymized):
|
||||||
|
|
||||||
|
{
|
||||||
|
"managers": [...],
|
||||||
|
"directs": [
|
||||||
|
{
|
||||||
|
"smtp": "direct.report@mgb.ch",
|
||||||
|
"userPrincipalName": "direct.report@mgb.ch",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
directs = json_obj.get("directs", [])
|
||||||
|
emails: List[str] = []
|
||||||
|
for item in directs:
|
||||||
|
email = item.get("smtp") or item.get("userPrincipalName")
|
||||||
|
if email:
|
||||||
|
emails.append(email)
|
||||||
|
return emails
|
||||||
|
|
||||||
|
|
||||||
|
def parse_workingwith_entries(json_obj: dict[str, Any]) -> List[Person]:
|
||||||
|
"""
|
||||||
|
Erwartete Struktur (aus working_with_anonymized):
|
||||||
|
|
||||||
|
{
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"email": "user@migros.ch",
|
||||||
|
"userPrincipalName": "user@migros.ch",
|
||||||
|
"fullName": "Nachname, Vorname-MIGROS",
|
||||||
|
"jobTitle": "Leitung",
|
||||||
|
"department": "Dept 1",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
value = json_obj.get("value", [])
|
||||||
|
persons: List[Person] = []
|
||||||
|
for item in value:
|
||||||
|
email = item.get("email") or item.get("userPrincipalName")
|
||||||
|
persons.append(
|
||||||
|
Person(
|
||||||
|
email=email,
|
||||||
|
display_name=item.get("fullName"),
|
||||||
|
job_title=item.get("jobTitle"),
|
||||||
|
department=item.get("department"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return persons
|
||||||
18
install.bat
Normal file
18
install.bat
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM Virtuelle Umgebung erstellen
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
REM venv aktivieren
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
REM Abhängigkeiten installieren
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
REM Playwright-Browser installieren
|
||||||
|
playwright install chromium
|
||||||
|
|
||||||
|
echo Installation abgeschlossen.
|
||||||
|
endlocal
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
playwright
|
||||||
|
pyvis
|
||||||
|
plotly
|
||||||
|
pandas
|
||||||
17
run.bat
Normal file
17
run.bat
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM Virtuelle Umgebung aktivieren
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
if "%1"=="" (
|
||||||
|
echo Nutzung:
|
||||||
|
echo run.bat login ^<email^>
|
||||||
|
echo run.bat manager ^<manager_email^>
|
||||||
|
echo run.bat emails ^<email1^> [^<email2^> ...]
|
||||||
|
goto :eof
|
||||||
|
)
|
||||||
|
|
||||||
|
python -m app.main %*
|
||||||
|
|
||||||
|
endlocal
|
||||||
15
visualize.bat
Normal file
15
visualize.bat
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM Virtuelle Umgebung aktivieren
|
||||||
|
call venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
if "%1"=="" (
|
||||||
|
echo Nutzung:
|
||||||
|
echo visualize.bat ^<path_to_csv^>
|
||||||
|
goto :eof
|
||||||
|
)
|
||||||
|
|
||||||
|
python -m app.easyvisualize.py %*
|
||||||
|
|
||||||
|
endlocal
|
||||||
Reference in New Issue
Block a user