Skip to content

Commit fb12f49

Browse files
add typescoring script to main (Azure#38906)
* add typescoring script to main repo * small updates for oct * add typescoring script * add to aggregate reports yaml * redefine ignore filters * fix date * remove diff * test run * update date format * revert back to monday * don't redefine filters
1 parent 8c01872 commit fb12f49

File tree

3 files changed

+326
-0
lines changed

3 files changed

+326
-0
lines changed

eng/pipelines/aggregate-reports.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ stages:
7777
scriptPath: 'scripts/repo_health_status_report/output_health_report.py'
7878
displayName: 'Generate Health Status Report'
7979

80+
- script: |
81+
python -m pip install -r scripts/repo_type_completeness/dev_requirements.txt
82+
displayName: 'Prep Environment'
83+
84+
- task: PythonScript@0
85+
condition: succeededOrFailed()
86+
env:
87+
GH_TOKEN: $(azuresdk-github-pat)
88+
inputs:
89+
scriptPath: 'scripts/repo_type_completeness/generate_main_typescores.py'
90+
displayName: 'Update Type Completeness Scores'
91+
8092
- template: ../common/pipelines/templates/steps/verify-links.yml
8193
parameters:
8294
Directory: ""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-e ./tools/azure-sdk-tools
2+
requests==2.28.1
3+
pyright==1.1.287
4+
PyGitHub>=1.59.0
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
from __future__ import annotations
7+
8+
import os
9+
import sys
10+
import datetime
11+
import logging
12+
import json
13+
import pathlib
14+
import glob
15+
import subprocess
16+
import csv
17+
import calendar
18+
from typing import Any
19+
20+
import requests
21+
from github import Github, Auth
22+
23+
from ci_tools.parsing import ParsedSetup
24+
from ci_tools.environment_exclusions import (
25+
IGNORE_PACKAGES,
26+
is_check_enabled,
27+
FILTER_EXCLUSIONS,
28+
IGNORE_FILTER,
29+
)
30+
31+
logging.getLogger().setLevel(logging.INFO)
32+
33+
INACTIVE_CLASSIFIER = "Development Status :: 7 - Inactive"
34+
35+
IGNORE_FILTER.append("mgmt")
36+
FILTER_EXCLUSIONS.append("azure-mgmt-core")
37+
IGNORE_PACKAGES.extend([
38+
"azure-openai",
39+
"azure-storage-extensions",
40+
"azure-schemaregistry-avroserializer",
41+
"azure-eventhub-checkpointstoretable",
42+
])
43+
44+
SDK_TEAM_OWNED_PACKAGES = [
45+
"azure-ai-documentintelligence",
46+
"azure-appconfiguration",
47+
"azure-containerregistry",
48+
"azure-core",
49+
"azure-mgmt-core",
50+
"azure-core-experimental",
51+
"azure-core-tracing-opencensus",
52+
"azure-core-tracing-openetelemetry",
53+
"azure-data-tables",
54+
"azure-eventgrid",
55+
"azure-eventhub",
56+
"azure-identity",
57+
"azure-keyvault-administration",
58+
"azure-keyvault-certificates",
59+
"azure-keyvault-keys",
60+
"azure-keyvault-secrets",
61+
"azure-monitor-ingestion",
62+
"azure-monitor-query",
63+
"azure-schemaregistry",
64+
"azure-search-documents",
65+
"azure-servicebus",
66+
"corehttp",
67+
]
68+
69+
70+
# We install each library from the dev feed. This installs mostly alpha versions.
71+
# These libraries have dependencies on other libraries where an alpha version is not
72+
# considered compatible with the given version specifier. Therefore, we add a second step
73+
# to install/score these libraries separately.
74+
75+
# Format: "library_to_score": [dependencies_to_uninstall]
76+
RESOLUTION_IMPOSSIBLE_LIBRARIES = {
77+
"azure-mixedreality-authentication": ["azure-mixedreality-remoterendering"],
78+
"azure-ai-ml": ["azure-storage-blob", "azure-storage-file-share", "azure-storage-file-datalake"],
79+
"azure-storage-blob-changefeed": ["azure-storage-blob"],
80+
"azure-storage-file-datalake": ["azure-storage-blob"],
81+
"azure-core": ["azure-core-experimental", "azure-core-tracing-opencensus", "azure-core-tracing-opentelemetry"],
82+
"azure-monitor-opentelemetry": ["azure-core-tracing-opentelemetry"],
83+
"azure-ai-evaluation": ["azure-monitor-opentelemetry-exporter", "azure-monitor-opentelemetry"],
84+
"azure-ai-generative": ["azure-ai-resources"],
85+
}
86+
87+
88+
def add_entity(package: str, packages_to_score: dict[str, Any], entities: list[dict[str, Any]]) -> None:
89+
d = packages_to_score[package]["Date"]
90+
entity = {
91+
"Package": package,
92+
"Date": d,
93+
"LatestVersion": packages_to_score[package]["LatestVersion"],
94+
"Score": packages_to_score[package]["Score"],
95+
"PyTyped": packages_to_score[package]["PyTyped"],
96+
"Pyright": packages_to_score[package]["Pyright"],
97+
"Mypy": packages_to_score[package]["Mypy"],
98+
"Samples": packages_to_score[package]["Samples"],
99+
"Verifytypes": packages_to_score[package]["Verifytypes"],
100+
"SDKTeamOwned": packages_to_score[package]["SDKTeamOwned"],
101+
"Active": packages_to_score[package]["Active"]
102+
}
103+
entities.append(entity)
104+
105+
106+
def install(packages: list[str]) -> None:
107+
commands = [
108+
sys.executable,
109+
"-m",
110+
"pip",
111+
"install",
112+
]
113+
114+
commands.extend(packages)
115+
subprocess.check_call(commands)
116+
117+
118+
def uninstall_deps(deps: list[str]) -> None:
119+
commands = [
120+
sys.executable,
121+
"-m",
122+
"pip",
123+
"uninstall",
124+
"-y"
125+
]
126+
127+
commands.extend(deps)
128+
subprocess.check_call(commands)
129+
130+
131+
def score_package(package: str, packages_to_score: dict[str, Any], entities: list[dict[str, Any]]) -> None:
132+
try:
133+
logging.info(f"Running verifytypes on {package}")
134+
commands = [sys.executable, "-m", "pyright", "--verifytypes", packages_to_score[package]["Module"], "--ignoreexternal", "--outputjson"]
135+
response = subprocess.run(
136+
commands,
137+
check=True,
138+
capture_output=True,
139+
)
140+
except subprocess.CalledProcessError as e:
141+
if e.returncode != 1:
142+
logging.info(
143+
f"Running verifytypes for {package} failed: {e.stderr}"
144+
)
145+
else:
146+
report = json.loads(e.output)
147+
else:
148+
report = json.loads(response.stdout) # package scores 100%
149+
pytyped_present = False if report["typeCompleteness"].get("pyTypedPath", None) is None else True
150+
packages_to_score[package].update({"PyTyped": pytyped_present})
151+
packages_to_score[package].update({"Score": round(report["typeCompleteness"]["completenessScore"] * 100, 1)})
152+
add_entity(package, packages_to_score, entities)
153+
154+
155+
def get_alpha_installs(packages_to_score: dict[str, Any]) -> tuple[list[str], dict[str, list[str]]]:
156+
feed_id = "3572dbf9-b5ef-433b-9137-fc4d7768e7cc"
157+
feed_resp = requests.get(f"https://feeds.dev.azure.com/azure-sdk/public/_apis/packaging/feeds/{feed_id}/Packages?api-version=7.0")
158+
packages = json.loads(feed_resp.text)
159+
versions_to_install = {}
160+
161+
# Dev feed doesn't default to latest version, so we have to check publish dates for most recent
162+
for package in packages["value"]:
163+
if package["name"] in packages_to_score:
164+
url = f"{package['url']}/Versions"
165+
versions = requests.get(url)
166+
version_list = json.loads(versions.text)["value"]
167+
latest_version = max(version_list, key=lambda v: datetime.datetime.strptime(v["publishDate"].split(".")[0], "%Y-%m-%dT%H:%M:%S"))
168+
versions_to_install[package["name"]] = latest_version["version"]
169+
170+
extra_index_url = ("--extra-index-url", "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple", "--pre")
171+
first_round = []
172+
second_round = {}
173+
174+
for package_name, version in versions_to_install.items():
175+
packages_to_score[package_name].update({'LatestVersion': version})
176+
package_info = [f"{package_name}=={version}", *extra_index_url]
177+
178+
if package_name in RESOLUTION_IMPOSSIBLE_LIBRARIES:
179+
second_round[package_name] = package_info
180+
else:
181+
first_round.extend(package_info)
182+
183+
return first_round, second_round
184+
185+
186+
def is_package_inactive(package_path: str) -> bool:
187+
return INACTIVE_CLASSIFIER in ParsedSetup.from_path(package_path).classifiers
188+
189+
190+
def skip_package(package_name: str) -> bool:
191+
return (
192+
(not package_name.startswith("azure") and package_name != "corehttp")
193+
or package_name in IGNORE_PACKAGES
194+
or package_name not in FILTER_EXCLUSIONS
195+
and any(identifier in package_name for identifier in IGNORE_FILTER)
196+
)
197+
198+
199+
def get_packages_to_score() -> dict[str, dict[str, Any]]:
200+
dataplane = {}
201+
sdk_path = pathlib.Path(__file__).parent.parent.parent / "sdk"
202+
service_directories = glob.glob(f"{sdk_path}/*/", recursive=True)
203+
today = datetime.datetime.today()
204+
for service in service_directories:
205+
package_paths = glob.glob(f"{service}*/", recursive=True)
206+
for pkg_path in package_paths:
207+
package_path = pathlib.Path(pkg_path)
208+
package_name = package_path.name
209+
if skip_package(package_name):
210+
continue
211+
package_path = str(package_path)
212+
package_info = ParsedSetup.from_path(package_path)
213+
dataplane[package_name] = {"LatestVersion": package_info.version}
214+
dataplane[package_name].update({"Path": package_path})
215+
dataplane[package_name].update({"Module": package_info.namespace})
216+
dataplane[package_name].update({"Pyright": is_check_enabled(package_path, "pyright")})
217+
dataplane[package_name].update({"Mypy": is_check_enabled(package_path, "mypy")})
218+
dataplane[package_name].update({"Samples": is_check_enabled(package_path, "type_check_samples")})
219+
dataplane[package_name].update({"Verifytypes": is_check_enabled(package_path, "verifytypes")})
220+
dataplane[package_name].update({"Date": today})
221+
dataplane[package_name].update({"SDKTeamOwned": package_name in SDK_TEAM_OWNED_PACKAGES})
222+
dataplane[package_name].update({"Active": not is_package_inactive(package_path)})
223+
224+
sorted_libs = {key: dataplane[key] for key in sorted(dataplane)}
225+
return sorted_libs
226+
227+
228+
def append_results_to_csv(entities: list[dict[str, Any]]) -> None:
229+
typescore_path = pathlib.Path(__file__).parent
230+
231+
github = Github(auth=Auth.Token(os.environ["GH_TOKEN"]))
232+
repo = github.get_repo("Azure/azure-sdk-for-python")
233+
234+
path = "scripts/repo_type_completeness/typescores.csv"
235+
content = repo.get_contents(path, ref="python-sdk-type-completeness")
236+
decoded_content = content.decoded_content.decode("utf-8")
237+
238+
with open(typescore_path / "typescores.csv", mode="a", newline='', encoding="utf-8") as file:
239+
file.write(decoded_content)
240+
writer = csv.writer(file)
241+
242+
for data in entities:
243+
row = [
244+
data["Package"],
245+
data["Date"].strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
246+
data["LatestVersion"],
247+
data["Score"],
248+
data["PyTyped"],
249+
data["Pyright"],
250+
data["Mypy"],
251+
data["Samples"],
252+
data["Verifytypes"],
253+
data["SDKTeamOwned"],
254+
data["Active"],
255+
]
256+
writer.writerow(row)
257+
258+
repo.update_file(
259+
path=path,
260+
message="Update typing dashboard",
261+
content=open(path, "rb").read(),
262+
branch="python-sdk-type-completeness",
263+
sha=content.sha
264+
)
265+
266+
267+
def should_run() -> bool:
268+
"""We only update the typing dashboard once a month on the Monday after release week.
269+
"""
270+
today = datetime.date.today()
271+
c = calendar.Calendar(firstweekday=calendar.SUNDAY)
272+
month_dates = c.monthdatescalendar(today.year, today.month)
273+
274+
monday_after_release_week = None
275+
for week in month_dates:
276+
for day in week:
277+
if day.weekday() == calendar.FRIDAY and day.month == today.month:
278+
monday_after_release_week = day + datetime.timedelta(days=10)
279+
return today == monday_after_release_week
280+
281+
return False
282+
283+
284+
def update_main_typescores() -> None:
285+
if not should_run():
286+
logging.info(f"Skipping type scoring update - only runs once a month on the Monday after release week.")
287+
return
288+
289+
packages_to_score = get_packages_to_score()
290+
291+
first_round, second_round = get_alpha_installs(packages_to_score)
292+
293+
install(first_round)
294+
295+
entities = []
296+
for package, _ in packages_to_score.items():
297+
if package in RESOLUTION_IMPOSSIBLE_LIBRARIES:
298+
continue
299+
score_package(package, packages_to_score, entities)
300+
301+
for package, deps in RESOLUTION_IMPOSSIBLE_LIBRARIES.items():
302+
uninstall_deps(deps)
303+
install(second_round[package])
304+
score_package(package, packages_to_score, entities)
305+
306+
append_results_to_csv(entities)
307+
308+
309+
if __name__ == "__main__":
310+
update_main_typescores()

0 commit comments

Comments
 (0)