jetbrains-plugin-server 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. jetbrains_plugin_server-0.0.1/LICENSE.md +21 -0
  2. jetbrains_plugin_server-0.0.1/PKG-INFO +44 -0
  3. jetbrains_plugin_server-0.0.1/README.md +15 -0
  4. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/__init__.py +1 -0
  5. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/config.py +20 -0
  6. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/model/__init__.py +0 -0
  7. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/model/data_listing.py +50 -0
  8. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/plugin_catalog.py +18 -0
  9. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/plugin_model.py +35 -0
  10. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/schemas.py +63 -0
  11. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/server.py +41 -0
  12. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/to_xml.py +29 -0
  13. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/tools/__init__.py +0 -0
  14. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/tools/dl_data.py +55 -0
  15. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server/tools/gen_json_cache.py +71 -0
  16. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server.egg-info/PKG-INFO +44 -0
  17. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server.egg-info/SOURCES.txt +23 -0
  18. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server.egg-info/dependency_links.txt +1 -0
  19. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server.egg-info/requires.txt +11 -0
  20. jetbrains_plugin_server-0.0.1/jetbrains_plugin_server.egg-info/top_level.txt +1 -0
  21. jetbrains_plugin_server-0.0.1/pyproject.toml +38 -0
  22. jetbrains_plugin_server-0.0.1/requirements.txt +8 -0
  23. jetbrains_plugin_server-0.0.1/setup.cfg +4 -0
  24. jetbrains_plugin_server-0.0.1/tests/test_normalize.py +24 -0
  25. jetbrains_plugin_server-0.0.1/tests/test_plugins.py +38 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 azro352
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: jetbrains-plugin-server
3
+ Version: 0.0.1
4
+ Summary: A server to serve jetbrains plugins
5
+ Author-email: azro352 <35503478+azro352@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/azro352/jetbrains-plugin-server.git
8
+ Project-URL: Bug Tracker, https://github.com/azro352/jetbrains-plugin-server/issues
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.md
18
+ Requires-Dist: fastapi~=0.115.12
19
+ Requires-Dist: markdown~=3.8
20
+ Requires-Dist: parameterized~=0.9.0
21
+ Requires-Dist: pydantic~=2.11.3
22
+ Requires-Dist: pydantic-extra-types[semver]~=2.10.3
23
+ Requires-Dist: requests~=2.32.3
24
+ Requires-Dist: semver~=3.0.4
25
+ Requires-Dist: uvicorn
26
+ Provides-Extra: offline
27
+ Requires-Dist: fastapi_offline; extra == "offline"
28
+ Dynamic: license-file
29
+
30
+ <!--
31
+
32
+ THIS README FILE IS RENDERED ON '/' ENDPOINT WHEN NO "build" ARG IS GIVEN
33
+
34
+ -->
35
+
36
+ # jetbrains-plugin-server
37
+
38
+ Creates a jetbrains-compatible plugin server with a given list of plugins
39
+
40
+ ## Tools
41
+
42
+ - `src/tools/dl_data.py` to fetch plugins specifications, versions and content from jetbrains to a local filesystem
43
+ - `src/tools/gen_json_cache.py` to build a JSON cache to answer faster, using either a filesystem storage or an
44
+ artifactory
@@ -0,0 +1,15 @@
1
+ <!--
2
+
3
+ THIS README FILE IS RENDERED ON '/' ENDPOINT WHEN NO "build" ARG IS GIVEN
4
+
5
+ -->
6
+
7
+ # jetbrains-plugin-server
8
+
9
+ Creates a jetbrains-compatible plugin server with a given list of plugins
10
+
11
+ ## Tools
12
+
13
+ - `src/tools/dl_data.py` to fetch plugins specifications, versions and content from jetbrains to a local filesystem
14
+ - `src/tools/gen_json_cache.py` to build a JSON cache to answer faster, using either a filesystem storage or an
15
+ artifactory
@@ -0,0 +1 @@
1
+ __version__ = '0.0.1'
@@ -0,0 +1,20 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ DL_URL_BASE = os.getenv("DL_URL_BASE", "https://fake.url/{plugin_version_id}")
5
+
6
+ JETBRAINS_PLUGINS_HOST = "https://plugins.jetbrains.com"
7
+
8
+ LOCAL = Path(__file__).parent.parent.joinpath("local")
9
+
10
+ PLUGIN_PROD_DATA = Path(__file__).parent.parent.joinpath("plugins_prod.json")
11
+
12
+ PLUGIN_TEST_DATA = Path(__file__).parent.parent.joinpath("tests", "data", "plugins_test.json")
13
+
14
+ PLUGIN_SPECS_DIR = "plugin_specs"
15
+ PLUGIN_VERSIONS_DIR = "plugin_versions"
16
+ PLUGINS_DIR = "plugins"
17
+
18
+ IS_TEST_MODE = os.getenv("TEST_MODE") == "true"
19
+
20
+ FAST_API_OFFLINE = os.getenv("FAST_API_OFFLINE") == "true"
@@ -0,0 +1,50 @@
1
+ from abc import ABC, abstractmethod
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+
8
+ class DataListing(ABC):
9
+ @abstractmethod
10
+ def list(self, directory) -> list[str]:
11
+ raise NotImplementedError()
12
+
13
+ @abstractmethod
14
+ def get(self, directory, file) -> Any:
15
+ raise NotImplementedError()
16
+
17
+
18
+ class FSDataListing(DataListing):
19
+
20
+ def __init__(self, base_path: str | Path):
21
+ self.base_path = Path(base_path)
22
+
23
+ def list(self, directory) -> list[str]:
24
+ return [
25
+ p.name
26
+ for p in self.base_path.joinpath(directory).glob("*")
27
+ ]
28
+
29
+ def get(self, directory, file) -> Any:
30
+ return self.base_path.joinpath(directory, file).read_text(encoding="utf8")
31
+
32
+
33
+ class ArtifactoryDataListing(DataListing):
34
+
35
+ def __init__(self, base_url: str, repo_name: str):
36
+ self.base_url = base_url.strip("/")
37
+ self.repo_name = repo_name.strip("/")
38
+
39
+ def list(self, directory) -> list[str]:
40
+ url = f"{self.base_url}/artifactory/api/storage/{self.repo_name}/{directory}"
41
+ files_rep = requests.get(url)
42
+ files = files_rep.json()
43
+ return [
44
+ Path(file["uri"]).name
45
+ for file in files["children"]
46
+ ]
47
+
48
+ def get(self, directory, file) -> Any:
49
+ url = f"{self.base_url}/artifactory/{self.repo_name}/{directory}/{file}"
50
+ return requests.get(url).text
@@ -0,0 +1,18 @@
1
+ import json
2
+ import logging
3
+ from functools import lru_cache
4
+
5
+ from jetbrains_plugin_server.config import PLUGIN_TEST_DATA, IS_TEST_MODE, PLUGIN_PROD_DATA
6
+ from jetbrains_plugin_server.schemas import CatalogSchema
7
+
8
+ LOG = logging.getLogger(__name__)
9
+
10
+
11
+ @lru_cache()
12
+ def get_plugin_catalog() -> CatalogSchema:
13
+ LOG.info("load catalog from file")
14
+ if IS_TEST_MODE:
15
+ catalog_path = PLUGIN_TEST_DATA
16
+ else:
17
+ catalog_path = PLUGIN_PROD_DATA
18
+ return CatalogSchema.model_validate(json.loads(catalog_path.read_text()))
@@ -0,0 +1,35 @@
1
+ from pydantic_extra_types.semantic_version import SemanticVersion
2
+
3
+ from jetbrains_plugin_server.plugin_catalog import get_plugin_catalog
4
+ from jetbrains_plugin_server.schemas import PluginVersionSchema, LowPaddingSemanticVersion
5
+
6
+
7
+ def get_plugins(build: str) -> list[tuple[str, PluginVersionSchema]]:
8
+ catalog = get_plugin_catalog()
9
+ build_version = LowPaddingSemanticVersion.parse(build)
10
+ return [
11
+ (plugin.name, version_found)
12
+ for plugin in catalog.plugins
13
+ if (version_found := get_latest_compatible(build_version, plugin.versions))
14
+ ]
15
+
16
+
17
+ def get_latest_compatible(
18
+ build_version: SemanticVersion,
19
+ plugin_versions: list[PluginVersionSchema]
20
+ ):
21
+ for plugin_version in plugin_versions:
22
+ if is_compatible(build_version, plugin_version):
23
+ return plugin_version
24
+
25
+ return None
26
+
27
+
28
+ def is_compatible(
29
+ build_version: SemanticVersion,
30
+ plugin_version: PluginVersionSchema
31
+ ) -> bool:
32
+ since, until = plugin_version.specs.since_build_semver, plugin_version.specs.until_build_semver
33
+ if until:
34
+ return since <= build_version <= until
35
+ return since <= build_version
@@ -0,0 +1,63 @@
1
+ import re
2
+ from typing import Literal, Type, Any
3
+
4
+ from pydantic import BaseModel, Field, model_validator
5
+ from pydantic_extra_types.semantic_version import SemanticVersion
6
+ from semver._types import String
7
+ from semver.version import T
8
+
9
+
10
+ class LowPaddingSemanticVersion:
11
+ @staticmethod
12
+ def parse(version: str) -> SemanticVersion:
13
+ return SemanticVersion.parse(normalize_version(version, "start"))
14
+
15
+
16
+ class HighPaddingSemanticVersion:
17
+ @staticmethod
18
+ def parse(version: str) -> SemanticVersion:
19
+ return SemanticVersion.parse(normalize_version(version, "end"))
20
+
21
+
22
+ def normalize_version(value: str, mode: Literal["start", "end"]):
23
+ if match := re.match(r"^[A-Z]+-(.+)$", value):
24
+ value = match.group(1)
25
+ replacer = "0" if mode == "start" else "999999"
26
+ result = value.replace(".*", f".{replacer}")
27
+ while result.count(".") < 2:
28
+ result += f".{replacer}"
29
+ return result
30
+
31
+
32
+ class PluginVersionSpecSchema(BaseModel):
33
+ since_build: str = Field(alias="since-build")
34
+ until_build: str | None = Field(alias="until-build", default=None)
35
+
36
+ since_build_semver: SemanticVersion = Field(alias="since-build-semver")
37
+ until_build_semver: SemanticVersion | None = Field(alias="until-build-semver", default=None)
38
+
39
+ @model_validator(mode='before')
40
+ @classmethod
41
+ def set_semver_fields(cls, data: Any) -> Any:
42
+ data["since-build-semver"] = LowPaddingSemanticVersion.parse(data["since-build"])
43
+ if ub := data.get("until-build"):
44
+ data["until-build-semver"] = HighPaddingSemanticVersion.parse(ub)
45
+ return data
46
+
47
+
48
+ class PluginVersionSchema(BaseModel):
49
+ plugin_id: str
50
+ plugin_version_id: int
51
+ description: str | None
52
+ change_notes: str | None
53
+ version: str
54
+ specs: PluginVersionSpecSchema
55
+
56
+
57
+ class PluginSchema(BaseModel):
58
+ name: str
59
+ versions: list[PluginVersionSchema]
60
+
61
+
62
+ class CatalogSchema(BaseModel):
63
+ plugins: list[PluginSchema] = Field(default_factory=list)
@@ -0,0 +1,41 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+
5
+ import markdown
6
+
7
+ from jetbrains_plugin_server.config import FAST_API_OFFLINE
8
+
9
+ if FAST_API_OFFLINE:
10
+ from fastapi_offline import FastAPIOffline as FastAPI
11
+ else:
12
+ from fastapi import FastAPI # type: ignore
13
+
14
+ from fastapi.responses import HTMLResponse, Response
15
+
16
+ from jetbrains_plugin_server.plugin_catalog import get_plugin_catalog
17
+ from jetbrains_plugin_server.plugin_model import get_plugins
18
+ from jetbrains_plugin_server.to_xml import to_xml
19
+
20
+ LOG = logging.getLogger(__name__)
21
+
22
+
23
+ def create_app():
24
+ app = FastAPI()
25
+
26
+ @app.get("/")
27
+ def get_plugins_route(build: Annotated[
28
+ str, "IDE build number to filter the available plugins and return only the compatible ones"] = ""):
29
+ if not build:
30
+ md = Path(__file__).parent.parent.joinpath("README.md").read_text()
31
+ return HTMLResponse(content=markdown.markdown(md))
32
+
33
+ LOG.debug("Request with build=%s", build)
34
+ result = get_plugins(build)
35
+ return Response(content=to_xml(result), media_type="application/xml")
36
+
37
+ @app.get("/cache")
38
+ def get_cache_route():
39
+ return get_plugin_catalog()
40
+
41
+ return app
@@ -0,0 +1,29 @@
1
+ import xml.etree.ElementTree as ET
2
+ from io import BytesIO
3
+
4
+ from jetbrains_plugin_server.config import DL_URL_BASE
5
+ from jetbrains_plugin_server.schemas import PluginVersionSchema
6
+
7
+
8
+ def to_xml(plugins: list[tuple[str, PluginVersionSchema]]):
9
+ a = ET.Element('plugins')
10
+ for name, plugin in plugins:
11
+ p = ET.SubElement(a, 'plugin', {
12
+ "id": plugin.plugin_id,
13
+ # url of download
14
+ "url": DL_URL_BASE.format(plugin_version_id=plugin.plugin_version_id),
15
+ "version": plugin.version
16
+ })
17
+ attrs = {"since-build": plugin.specs.since_build}
18
+ if ub := plugin.specs.until_build:
19
+ attrs["until-build"] = ub
20
+ iv = ET.SubElement(p, 'idea-version', attrs)
21
+
22
+ ET.SubElement(p, 'name').text = name
23
+ ET.SubElement(p, 'description').text = plugin.description
24
+ ET.SubElement(p, 'change-notes').text = plugin.change_notes
25
+
26
+ bio = BytesIO()
27
+ ET.ElementTree(a).write(bio)
28
+ bio.seek(0)
29
+ return b"<?xml version='1.0' encoding='UTF-8'?>" + bio.read()
@@ -0,0 +1,55 @@
1
+ from requests import get, Session
2
+ from requests.adapters import HTTPAdapter, Retry
3
+
4
+ from jetbrains_plugin_server.config import PLUGINS_DIR, PLUGIN_VERSIONS_DIR, PLUGIN_SPECS_DIR, LOCAL, \
5
+ JETBRAINS_PLUGINS_HOST
6
+
7
+ PLUGINS: list[str] = [
8
+ "https://plugins.jetbrains.com/plugin/631-python",
9
+ "https://plugins.jetbrains.com/plugin/10080-rainbow-brackets",
10
+ "https://plugins.jetbrains.com/plugin/11938-one-dark-theme",
11
+ "https://plugins.jetbrains.com/plugin/9525--env-files",
12
+ "https://plugins.jetbrains.com/plugin/11938-one-dark-theme",
13
+ "https://plugins.jetbrains.com/plugin/15075-jpa-buddy",
14
+ "https://plugins.jetbrains.com/plugin/9525--env-files-support/",
15
+ "https://plugins.jetbrains.com/plugin/10044-atom-material-icons/",
16
+ "https://plugins.jetbrains.com/plugin/164-ideavim",
17
+ "https://plugins.jetbrains.com/plugin/9792-key-promoter-x",
18
+ "https://plugins.jetbrains.com/plugin/14708-mario-progress-bar",
19
+ "https://plugins.jetbrains.com/plugin/7086-acejump"
20
+ ]
21
+
22
+ if __name__ == '__main__':
23
+ s = Session()
24
+ retries = Retry(total=5, backoff_factor=0.1)
25
+ s.mount('https://', HTTPAdapter(max_retries=retries))
26
+
27
+ for plugin in PLUGINS:
28
+ print("PLUGIN", plugin)
29
+ plugin = plugin.replace(f"{JETBRAINS_PLUGINS_HOST}/plugin/", "").strip("/")
30
+ plugin_id_int = plugin.split("-", maxsplit=1)[0]
31
+
32
+ versions_rep = get(f"{JETBRAINS_PLUGINS_HOST}/plugins/list?pluginId={plugin}")
33
+ LOCAL.joinpath(PLUGIN_SPECS_DIR, f"{plugin_id_int}.xml").write_bytes(
34
+ versions_rep.content
35
+ )
36
+
37
+ versions_id_rep = get(f"{JETBRAINS_PLUGINS_HOST}/api/plugins/{plugin_id_int}/updateVersions")
38
+ LOCAL.joinpath(PLUGIN_VERSIONS_DIR, f"{plugin_id_int}.json").write_bytes(
39
+ versions_id_rep.content
40
+ )
41
+
42
+ for row in versions_id_rep.json()[:-4:-1]:
43
+ print(" VERSION", row["version"])
44
+ plugin_version_id = row["id"]
45
+ dl = s.get(
46
+ f"{JETBRAINS_PLUGINS_HOST}/plugin/download",
47
+ params={"updateId": plugin_version_id},
48
+ stream=True
49
+ )
50
+
51
+ print(" Done in ", dl.elapsed)
52
+
53
+ LOCAL.joinpath(PLUGINS_DIR, f"{plugin_version_id}.zip").write_bytes(
54
+ dl.content
55
+ )
@@ -0,0 +1,71 @@
1
+ import json
2
+ import logging
3
+ import xml.etree.ElementTree as ET
4
+ from typing import cast
5
+
6
+ from jetbrains_plugin_server.config import PLUGIN_TEST_DATA, PLUGIN_SPECS_DIR, PLUGIN_VERSIONS_DIR, \
7
+ PLUGIN_PROD_DATA, IS_TEST_MODE, \
8
+ PLUGINS_DIR
9
+ from jetbrains_plugin_server.model.data_listing import DataListing
10
+ from jetbrains_plugin_server.schemas import CatalogSchema, PluginSchema, PluginVersionSchema, \
11
+ PluginVersionSpecSchema
12
+
13
+ LOG = logging.getLogger(__name__)
14
+
15
+
16
+ def gen_json_cache(dl: DataListing, only_available_plugins: bool = True):
17
+ """
18
+ dl = ArtifactoryDataListing("https://...", "jetbrains/plugin-server")
19
+ dl = FSDataListing(LOCAL)
20
+
21
+ gen_json_cache(dl)
22
+ """
23
+ LOG.info("build catalog")
24
+
25
+ result: CatalogSchema = CatalogSchema()
26
+
27
+ available_plugins = [int(p.split(".", maxsplit=1)[0]) for p in dl.list(PLUGINS_DIR)]
28
+
29
+ for plugin in dl.list(PLUGIN_SPECS_DIR):
30
+ plugin_id_int = plugin.split(".", maxsplit=1)[0]
31
+
32
+ versions_to_plugin_version_id = {
33
+ row["version"]: row["id"]
34
+ for row in json.loads(dl.get(PLUGIN_VERSIONS_DIR, f"{plugin_id_int}.json"))
35
+ }
36
+
37
+ plugin_spec = dl.get(PLUGIN_SPECS_DIR, plugin)
38
+
39
+ root = ET.fromstring(plugin_spec)
40
+
41
+ if not (category := root.find("category")):
42
+ LOG.error("Plugin %s has no category", plugin)
43
+ continue
44
+
45
+ versions = category.findall("idea-plugin")
46
+
47
+ result.plugins.append(PluginSchema(
48
+ name=versions[0].find("name").text,
49
+ versions=[
50
+ PluginVersionSchema(
51
+ plugin_id=version.find("id").text,
52
+ plugin_version_id=versions_to_plugin_version_id[version.find("version").text],
53
+ version=version.find("version").text or "",
54
+ specs=PluginVersionSpecSchema(
55
+ **{cast(str, k): v for k, v in version.find("idea-version").items() if v != "n/a"}
56
+ ),
57
+ description=version.find("description").text,
58
+ change_notes=version.find("change-notes").text,
59
+ )
60
+ for version in versions
61
+ if not only_available_plugins or
62
+ versions_to_plugin_version_id[version.find("version").text] in available_plugins
63
+ ]
64
+ ))
65
+ LOG.info("Plugin '%s' (%s) found %s versions but only %s had available zip",
66
+ versions[0].find("name").text, plugin, len(versions), len(result.plugins[-1].versions))
67
+
68
+ if IS_TEST_MODE:
69
+ PLUGIN_TEST_DATA.write_text(json.dumps(result.model_dump(mode="json", by_alias=True), indent=4))
70
+ else:
71
+ PLUGIN_PROD_DATA.write_text(json.dumps(result.model_dump(mode="json", by_alias=True), indent=4))
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: jetbrains-plugin-server
3
+ Version: 0.0.1
4
+ Summary: A server to serve jetbrains plugins
5
+ Author-email: azro352 <35503478+azro352@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/azro352/jetbrains-plugin-server.git
8
+ Project-URL: Bug Tracker, https://github.com/azro352/jetbrains-plugin-server/issues
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.md
18
+ Requires-Dist: fastapi~=0.115.12
19
+ Requires-Dist: markdown~=3.8
20
+ Requires-Dist: parameterized~=0.9.0
21
+ Requires-Dist: pydantic~=2.11.3
22
+ Requires-Dist: pydantic-extra-types[semver]~=2.10.3
23
+ Requires-Dist: requests~=2.32.3
24
+ Requires-Dist: semver~=3.0.4
25
+ Requires-Dist: uvicorn
26
+ Provides-Extra: offline
27
+ Requires-Dist: fastapi_offline; extra == "offline"
28
+ Dynamic: license-file
29
+
30
+ <!--
31
+
32
+ THIS README FILE IS RENDERED ON '/' ENDPOINT WHEN NO "build" ARG IS GIVEN
33
+
34
+ -->
35
+
36
+ # jetbrains-plugin-server
37
+
38
+ Creates a jetbrains-compatible plugin server with a given list of plugins
39
+
40
+ ## Tools
41
+
42
+ - `src/tools/dl_data.py` to fetch plugins specifications, versions and content from jetbrains to a local filesystem
43
+ - `src/tools/gen_json_cache.py` to build a JSON cache to answer faster, using either a filesystem storage or an
44
+ artifactory
@@ -0,0 +1,23 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ requirements.txt
5
+ jetbrains_plugin_server/__init__.py
6
+ jetbrains_plugin_server/config.py
7
+ jetbrains_plugin_server/plugin_catalog.py
8
+ jetbrains_plugin_server/plugin_model.py
9
+ jetbrains_plugin_server/schemas.py
10
+ jetbrains_plugin_server/server.py
11
+ jetbrains_plugin_server/to_xml.py
12
+ jetbrains_plugin_server.egg-info/PKG-INFO
13
+ jetbrains_plugin_server.egg-info/SOURCES.txt
14
+ jetbrains_plugin_server.egg-info/dependency_links.txt
15
+ jetbrains_plugin_server.egg-info/requires.txt
16
+ jetbrains_plugin_server.egg-info/top_level.txt
17
+ jetbrains_plugin_server/model/__init__.py
18
+ jetbrains_plugin_server/model/data_listing.py
19
+ jetbrains_plugin_server/tools/__init__.py
20
+ jetbrains_plugin_server/tools/dl_data.py
21
+ jetbrains_plugin_server/tools/gen_json_cache.py
22
+ tests/test_normalize.py
23
+ tests/test_plugins.py
@@ -0,0 +1,11 @@
1
+ fastapi~=0.115.12
2
+ markdown~=3.8
3
+ parameterized~=0.9.0
4
+ pydantic~=2.11.3
5
+ pydantic-extra-types[semver]~=2.10.3
6
+ requests~=2.32.3
7
+ semver~=3.0.4
8
+ uvicorn
9
+
10
+ [offline]
11
+ fastapi_offline
@@ -0,0 +1 @@
1
+ jetbrains_plugin_server
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools >= 77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jetbrains-plugin-server"
7
+ dynamic = ["dependencies", "version"]
8
+ requires-python = ">=3.10"
9
+ description = "A server to serve jetbrains plugins"
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ license-files = ["LICENSE.md"]
13
+ classifiers = [
14
+ "Intended Audience :: Developers",
15
+ "Development Status :: 4 - Beta",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+ authors = [
22
+ { name = "azro352", email = "35503478+azro352@users.noreply.github.com" },
23
+ ]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["."]
27
+ include = ["jetbrains_plugin_server*"]
28
+
29
+ [tool.setuptools.dynamic]
30
+ dependencies = { file = ["requirements.txt"] }
31
+ version = { attr = "jetbrains_plugin_server.__version__" }
32
+
33
+ [project.optional-dependencies]
34
+ offline = ["fastapi_offline"]
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/azro352/jetbrains-plugin-server.git"
38
+ "Bug Tracker" = "https://github.com/azro352/jetbrains-plugin-server/issues"
@@ -0,0 +1,8 @@
1
+ fastapi~=0.115.12
2
+ markdown~=3.8
3
+ parameterized~=0.9.0
4
+ pydantic~=2.11.3
5
+ pydantic-extra-types[semver]~=2.10.3
6
+ requests~=2.32.3
7
+ semver~=3.0.4
8
+ uvicorn
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ from unittest import TestCase
2
+
3
+ from parameterized import parameterized
4
+
5
+ from jetbrains_plugin_server.schemas import normalize_version
6
+
7
+
8
+ class TestNormalize(TestCase):
9
+
10
+ @parameterized.expand([
11
+ ["1", "1.0.0"],
12
+ ["1.0", "1.0.0"],
13
+ ["1.0.0", "1.0.0"],
14
+ ])
15
+ def test_normalize_start(self, version, expected):
16
+ self.assertEquals(normalize_version(version, "start"), expected)
17
+
18
+ @parameterized.expand([
19
+ ["1", "1.999999.999999"],
20
+ ["1.123", "1.123.999999"],
21
+ ["1.123.456", "1.123.456"],
22
+ ])
23
+ def test_normalize_end(self, version, expected):
24
+ self.assertEquals(normalize_version(version, "end"), expected)
@@ -0,0 +1,38 @@
1
+ import os
2
+ from unittest import TestCase
3
+
4
+ os.environ["TEST_MODE"] = "true"
5
+
6
+ from jetbrains_plugin_server.plugin_model import get_plugins
7
+
8
+ """
9
+ for c in content:
10
+ print(f'self.assertPluginExists(content, "{c[0]}", "{c[1].version}")')
11
+ """
12
+
13
+
14
+ class TestPlugin(TestCase):
15
+
16
+ def test_one(self):
17
+ content = get_plugins("243.26053")
18
+
19
+ self.assertPluginExists(content, "Python", "243.26053.27")
20
+ self.assertPluginExists(content, "Rainbow Brackets", "2024.2.11-241")
21
+ self.assertPluginExists(content, "One Dark Theme", "5.13.0")
22
+ self.assertPluginExists(content, ".env files", "2024.3")
23
+
24
+ def test_two(self):
25
+ content = get_plugins("222.4554.15")
26
+
27
+ self.assertPluginExists(content, "Python", "222.4554.10")
28
+ self.assertPluginExists(content, "Rainbow Brackets", "2023.3.7-ij")
29
+ self.assertPluginExists(content, "One Dark Theme", "5.13.0")
30
+ self.assertPluginExists(content, ".env files", "2022.2")
31
+
32
+ def assertPluginExists(self, plugins, name, version):
33
+ for pname, pvs in plugins:
34
+ if pname == name:
35
+ self.assertEquals(pvs.version, version, f"Invalid version for plugin {name}")
36
+ break
37
+ else:
38
+ self.fail(f"Plugin {name} not found")