Skip to content

Commit f302a31

Browse files
authored
Merge branch 'plugin_api_v2' into patch-5
2 parents 84e3426 + 73509bd commit f302a31

17 files changed

+2985
-2839
lines changed

.github/workflows/links.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v4
1616

17+
- name: Create plugins.json For All Plugins
18+
if: ${{ github.event_name == 'schedule' }}
19+
run: |
20+
python ./ci/src/merge-manifest.py
21+
22+
- name: Create plugins.json For New Plugins Only
23+
if: ${{ github.event_name == 'pull_request' }}
24+
run: |
25+
python ./ci/src/merge-manifest.py new-only
26+
1727
- name: Link Checker
1828
id: lychee
1929
uses: lycheeverse/lychee-action@v2

.github/workflows/python-passed.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ jobs:
1919
uses: actions/setup-python@v5
2020
with:
2121
python-version: 3.x
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -r ./ci/envs/requirements-update-tested.txt
2226
- name: Run script
23-
run: python ./ci/src/update-tested.py
27+
run: python ./ci/src/update-tested.py ${{ secrets.DISCORD_WEBHOOK }}
2428
- name: Update plugin manifest
2529
if: success()
2630
uses: stefanzweifel/git-auto-commit-action@v4
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
aiohttp
2+
tqdm

ci/src/_utils.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# -*-coding: utf-8 -*-
22
import json
3+
import os
4+
import re
35
from pathlib import Path
46
from typing import Dict, List, TypeVar
5-
import re
6-
import os
7+
8+
# If adding a third-party library here, check CI workflows Python files
9+
# that are dependant on this and require pip install.
710

811
# path
912
utils_path = Path(__file__).resolve()
@@ -17,7 +20,18 @@
1720
# constants
1821
id_name = "ID"
1922
language_name = "Language"
20-
language_list = ("csharp", "executable", "fsharp", "python", "javascript", "typescript", "python_v2", "executable_v2", "javascript_v2", "typescript_v2")
23+
language_list = (
24+
"csharp",
25+
"executable",
26+
"fsharp",
27+
"python",
28+
"javascript",
29+
"typescript",
30+
"python_v2",
31+
"executable_v2",
32+
"javascript_v2",
33+
"typescript_v2",
34+
)
2135
etag = "ETag"
2236
version = "Version"
2337
url_sourcecode = "UrlSourceCode"
@@ -30,6 +44,7 @@
3044
github_url = "https://github.com"
3145
release_date = "LatestReleaseDate"
3246
date_added = "DateAdded"
47+
website = "Website"
3348

3449
# typing
3550
PluginType = Dict[str, str]
@@ -51,12 +66,20 @@ def plugin_reader() -> P:
5166

5267
return manifests
5368

69+
70+
def save_plugins_json_file(content: list[dict[str]]) -> None:
71+
with open("plugins.json", "w", encoding="utf-8") as f:
72+
json.dump(content, f, indent=4, ensure_ascii=False)
73+
74+
5475
def get_plugin_file_paths() -> list[str]:
5576
return [os.path.join(plugin_dir, filename) for filename in get_plugin_filenames()]
5677

78+
5779
def get_plugin_filenames() -> list[str]:
5880
return os.listdir(plugin_dir)
5981

82+
6083
def etag_reader() -> ETagsType:
6184
with open(etag_file, "r", encoding="utf-8") as f:
6285
return json.load(f)
@@ -66,7 +89,8 @@ def plugin_writer(content: P):
6689
for plugin in content:
6790
with open(plugin_dir / f"{plugin[plugin_name]}-{plugin[id_name]}.json", "w", encoding="utf-8") as f:
6891
json.dump(plugin, f, indent=4)
69-
92+
93+
7094
def etags_writer(content: ETagsType):
7195
with open(etag_file, "w", encoding="utf-8") as f:
7296
json.dump(content, f, indent=4)
@@ -75,10 +99,12 @@ def etags_writer(content: ETagsType):
7599
def clean(string: str, flag="-") -> str:
76100
return string.lower().replace(flag, "").strip()
77101

102+
78103
def version_tuple(version: str) -> tuple:
79104
version = clean(version, "v")
80105
return tuple(version.split("."))
81106

107+
82108
def check_url(url: str) -> bool:
83109
regex = re.compile(
84110
r"^(?:http|ftp)s?://" # http:// or https://
@@ -100,3 +126,19 @@ def get_file_plugins_json_info(required_key: str = "") -> list[dict[str, str]]:
100126
return data
101127

102128
return [{required_key: plugin[required_key]} for plugin in data]
129+
130+
131+
def get_new_plugin_submission_ids() -> list[str]:
132+
plugins_json_ids = [item["ID"] for item in get_file_plugins_json_info("ID")]
133+
existing_plugin_file_ids = [info["ID"] for info in plugin_reader()]
134+
135+
new_ids = []
136+
137+
for id in existing_plugin_file_ids:
138+
# plugins.json would not contain new submission's ID.
139+
if id in plugins_json_ids:
140+
continue
141+
142+
new_ids.append(id)
143+
144+
return new_ids

ci/src/discord.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import aiohttp
2-
2+
from tqdm.asyncio import tqdm
33
from _utils import *
44

55
MAX_BODY_LEN = 1024
66

77

8-
async def update_hook(webhook_url: str, info: dict, latest_ver: str, release: dict) -> None:
8+
async def update_hook(webhook_url: str, info: PluginType, latest_ver: str, release: dict) -> None:
99
embed = {
1010
"content": None,
1111
"embeds": [
@@ -43,6 +43,43 @@ async def update_hook(webhook_url: str, info: dict, latest_ver: str, release: di
4343
embed['embeds'][0]['fields'].append({"name": "Release Notes", "value": truncate_release_notes(release['html_url'], release.get('body', ""))})
4444
async with aiohttp.ClientSession() as session:
4545
await session.post(webhook_url, json=embed)
46+
47+
async def release_hook(webhook_url: str, info: PluginType) -> None:
48+
embed = {
49+
"content": None,
50+
"embeds": [
51+
{
52+
"title": info[plugin_name],
53+
"description": f"New Plugin!\nReleased at v{info[version]}.",
54+
"url": info[website],
55+
"color": 5763719, # green
56+
"fields": [
57+
{
58+
"name": "Plugin Description",
59+
"value": info[description]
60+
},
61+
{
62+
"name": "Plugin Language",
63+
"value": info[language_name]
64+
}
65+
],
66+
"author": {
67+
"name": info[author]
68+
},
69+
"thumbnail": {
70+
"url": info[icon_path]
71+
}
72+
}
73+
]
74+
}
75+
if 'github.com' in info[url_sourcecode].lower():
76+
github_username = info[url_sourcecode].split('/')[3]
77+
embed['embeds'][0]['author']['name'] = github_username
78+
embed['embeds'][0]['author']['url'] = f"{github_url}/{github_username}"
79+
embed['embeds'][0]["author"]["icon_url"] = f"{github_url}/{github_username}.png?size=40"
80+
81+
async with aiohttp.ClientSession() as session:
82+
await session.post(webhook_url, json=embed)
4683

4784
def truncate_release_notes(url: str, release_notes: str, length: int = MAX_BODY_LEN) -> str:
4885
if len(release_notes) <= length:

ci/src/merge-manifest.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
import glob
2-
import json
1+
import sys
32

4-
if __name__ == "__main__":
5-
plugins = sorted(glob.glob("plugins/*.json"))
3+
from _utils import get_new_plugin_submission_ids, plugin_reader, save_plugins_json_file
4+
5+
6+
def get_all_plugins() -> list[dict[str]]:
7+
return plugin_reader()
8+
9+
10+
def get_new_plugins() -> list[dict[str]]:
11+
ids = get_new_plugin_submission_ids()
12+
plugins_from_plugins_dir = plugin_reader()
613

7-
manifests = []
14+
new_plugins = []
815

9-
for plugin in plugins:
10-
with open(plugin, "r", encoding="utf-8") as f:
11-
manifest = json.load(f)
12-
manifests.append(manifest)
16+
for id in ids:
17+
for plugin in plugins_from_plugins_dir:
18+
if plugin["ID"] == id:
19+
new_plugins.append(plugin)
20+
break
21+
22+
return new_plugins
23+
24+
25+
if __name__ == "__main__":
1326

14-
with open("plugins.json", "w", encoding="utf-8") as f:
15-
json.dump(manifests, f, indent=4, ensure_ascii=False)
27+
if len(sys.argv) > 1 and str(sys.argv[1]) == "new-only":
28+
save_plugins_json_file(get_new_plugins())
29+
else:
30+
save_plugins_json_file(get_all_plugins())

ci/src/update-tested.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
1-
import sys
2-
import json
3-
import os
4-
import zipfile
5-
import io
6-
from datetime import datetime
1+
import asyncio
2+
from datetime import datetime, UTC
3+
from sys import argv
4+
from _utils import plugin_reader, plugin_writer, date_added
5+
from discord import release_hook
76

8-
from _utils import clean, id_name, language_list, language_name, plugin_reader, plugin_writer, release_date, date_added
97

10-
if __name__ == "__main__":
8+
async def update_tested():
9+
webhook_url = None
10+
if len(argv) > 1:
11+
webhook_url = argv[1]
12+
1113
plugin_infos = plugin_reader()
1214

1315
for idx, plugin in enumerate(plugin_infos):
1416
if plugin["Language"] == "python" and "Tested" not in plugin.keys():
1517
plugin_infos[idx]["Tested"] = True
18+
1619
# Add date added if field is not present
1720
if plugin.get(date_added) is None:
18-
plugin_infos[idx][date_added] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
19-
plugin_writer(plugin_infos)
20-
21-
22-
21+
plugin_infos[idx][date_added] = datetime.now(UTC).strftime(
22+
"%Y-%m-%dT%H:%M:%SZ"
23+
)
2324

25+
if webhook_url:
26+
await release_hook(webhook_url, plugin)
2427

28+
plugin_writer(plugin_infos)
2529

2630

31+
if __name__ == "__main__":
32+
asyncio.run(update_tested())

ci/src/updater.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@
1212
from _utils import *
1313
from discord import update_hook
1414

15-
1615
async def batch_github_plugin_info(
17-
info: P, tags: ETagsType, github_token=None, webhook_url: str = None
16+
info: P, tags: ETagsType, github_token=None, webhook_url: str | None = None
1817
) -> P:
1918
try:
2019
headers = {"authorization": f"token {github_token}"}
@@ -47,9 +46,10 @@ async def batch_github_plugin_info(
4746
info[release_date] = latest_rel.get("published_at")
4847
if assets:
4948
info[url_download] = assets[0]["browser_download_url"]
50-
await send_notification(
51-
info, clean(latest_rel["tag_name"], "v"), latest_rel, webhook_url
52-
)
49+
if webhook_url:
50+
await send_notification(
51+
info, clean(latest_rel["tag_name"], "v"), latest_rel, webhook_url
52+
)
5353
info[version] = clean(latest_rel["tag_name"], "v")
5454

5555
tags[info[id_name]] = res.headers.get(etag, "")
@@ -62,7 +62,7 @@ async def batch_github_plugin_info(
6262

6363

6464
async def batch_plugin_infos(
65-
plugin_infos: Ps, tags: ETagsType, github_token, webhook_url: str = None
65+
plugin_infos: Ps, tags: ETagsType, github_token, webhook_url: str | None = None
6666
) -> Ps:
6767
return await tqdm.gather(
6868
*[
@@ -72,7 +72,7 @@ async def batch_plugin_infos(
7272
)
7373

7474

75-
def remove_unused_etags(plugin_infos: Ps, etags: ETagsType) -> ETagsType:
75+
def remove_unused_etags(plugin_infos: PluginsType, etags: ETagsType) -> ETagsType:
7676
etags_updated = {}
7777
plugin_ids = [info.get("ID") for info in plugin_infos]
7878

@@ -90,16 +90,18 @@ def remove_unused_etags(plugin_infos: Ps, etags: ETagsType) -> ETagsType:
9090

9191

9292
async def send_notification(
93-
info: P, latest_ver, release, webhook_url: str = None
93+
info: PluginType, latest_ver, release, webhook_url: str | None = None
9494
) -> None:
95+
if not webhook_url:
96+
return
97+
9598
if version_tuple(info[version]) != version_tuple(latest_ver):
9699
tqdm.write(f"Update detected: {info[plugin_name]} {latest_ver}")
97100
try:
98101
await update_hook(webhook_url, info, latest_ver, release)
99102
except Exception as e:
100103
tqdm.write(str(e))
101104

102-
103105
async def main():
104106
webhook_url = None
105107
if len(argv) > 1:

ci/src/validator.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*-coding: utf-8 -*-
22
import uuid
33

4-
from _utils import (check_url, clean, get_file_plugins_json_info, get_plugin_file_paths, get_plugin_filenames,
4+
from _utils import (check_url, clean, get_new_plugin_submission_ids, get_plugin_file_paths, get_plugin_filenames,
55
icon_path, id_name, language_list, language_name, plugin_reader)
66

77
plugin_infos = plugin_reader()
@@ -22,32 +22,29 @@ def test_language_in_list():
2222
msg = f"The '{language_name}' is not in the list of {language_list}"
2323
assert set(language_list) >= set(languages), msg
2424

25+
2526
def test_valid_icon_url():
2627
for plugin in plugin_infos:
2728
msg = f"The URL in {icon_path} is not a valid URL."
2829
assert check_url(plugin[icon_path]), msg
2930

31+
3032
def test_file_type_json():
3133
incorrect_ext_files = [file_path for file_path in get_plugin_file_paths() if not file_path.endswith(".json")]
3234

3335
assert len(incorrect_ext_files) == 0, f"Expected the following file to be of .json extension: {incorrect_ext_files}"
3436

37+
3538
def test_file_name_construct():
3639
filenames = get_plugin_filenames()
3740
for info in plugin_infos:
3841
assert (
3942
f"{info['Name']}-{info['ID']}.json" in filenames
4043
), f"Plugin {info['Name']} with ID {info['ID']} does not have the correct filename. Make sure it's name + ID, i.e. {info['Name']}-{info['ID']}.json"
4144

42-
def test_submitted_plugin_id_is_valid_uuid():
43-
plugins_json_ids = [item["ID"] for item in get_file_plugins_json_info("ID")]
44-
existing_plugin_file_ids = [info["ID"] for info in plugin_infos]
45-
46-
for id in existing_plugin_file_ids:
47-
# plugins.json would not contain new submission's ID.
48-
if id in plugins_json_ids:
49-
continue
5045

46+
def test_submitted_plugin_id_is_valid_uuid():
47+
for id in get_new_plugin_submission_ids():
5148
try:
5249
uuid.UUID(id, version=4)
5350
outcome = True

0 commit comments

Comments
 (0)