modforge-cli 0.1.5__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.
- modforge_cli-0.1.5/LICENSE +21 -0
- modforge_cli-0.1.5/PKG-INFO +70 -0
- modforge_cli-0.1.5/README.md +37 -0
- modforge_cli-0.1.5/pyproject.toml +55 -0
- modforge_cli-0.1.5/src/modforge_cli/__init__.py +5 -0
- modforge_cli-0.1.5/src/modforge_cli/__main__.py +8 -0
- modforge_cli-0.1.5/src/modforge_cli/__version__.py +5 -0
- modforge_cli-0.1.5/src/modforge_cli/api/__init__.py +7 -0
- modforge_cli-0.1.5/src/modforge_cli/api/modrinth.py +218 -0
- modforge_cli-0.1.5/src/modforge_cli/cli.py +559 -0
- modforge_cli-0.1.5/src/modforge_cli/core/__init__.py +6 -0
- modforge_cli-0.1.5/src/modforge_cli/core/downloader.py +89 -0
- modforge_cli-0.1.5/src/modforge_cli/core/models.py +72 -0
- modforge_cli-0.1.5/src/modforge_cli/core/policy.py +162 -0
- modforge_cli-0.1.5/src/modforge_cli/core/resolver.py +134 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Frank1o3
|
|
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,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modforge-cli
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: ModForge-CLI — a Modrinth-based Minecraft modpack builder
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Frank1o3
|
|
8
|
+
Author-email: jahdy1o3@gmail.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Dist: aiofiles (>=25.1.0,<26.0.0)
|
|
21
|
+
Requires-Dist: aiohttp (>=3.13.3,<4.0.0)
|
|
22
|
+
Requires-Dist: jsonschema (>=4.25.1,<5.0.0)
|
|
23
|
+
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
24
|
+
Requires-Dist: pyfiglet (>=1.0.4,<2.0.0)
|
|
25
|
+
Requires-Dist: pyzipper (>=0.3.6,<0.4.0)
|
|
26
|
+
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
27
|
+
Requires-Dist: rich (>=14.2.0,<15.0.0)
|
|
28
|
+
Requires-Dist: typer (>=0.21.1,<0.22.0)
|
|
29
|
+
Project-URL: Homepage, https://frank1o3.github.io/ModForge-CLI/
|
|
30
|
+
Project-URL: Repository, https://github.com/Frank1o3/ModForge-CLI
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# ModForge-CLI ⛏
|
|
34
|
+
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
[](https://docs.modrinth.com/api-spec)
|
|
38
|
+
|
|
39
|
+
**ModForge-CLI** is a powerful CLI tool for building and managing custom Minecraft modpacks using the Modrinth API v2.
|
|
40
|
+
|
|
41
|
+
Search for projects, fetch versions, validate manifests, download mods with hash checks, and generate complete files — all from the terminal.
|
|
42
|
+
|
|
43
|
+
Ideal for modpack developers, server admins, and automation scripts.
|
|
44
|
+
|
|
45
|
+
## Terminal Banner
|
|
46
|
+
|
|
47
|
+
When you run ModForge-CLI, you'll be greeted with this colorful Minecraft-themed banner
|
|
48
|
+
|
|
49
|
+
## Key Features
|
|
50
|
+
|
|
51
|
+
- **Modrinth API v2 Integration**: Search projects, list versions, fetch metadata in bulk.
|
|
52
|
+
- **Modpack Management**: Read/validate `modrinth.index.json`, build packs from metadata.
|
|
53
|
+
- **Validation**: Full JSON Schema checks + optional Pydantic models for strict typing.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Requires **Python 3.13+**.
|
|
58
|
+
|
|
59
|
+
**Recommended (Poetry)**:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
poetry install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Alternative (pip)**:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -r requirements.txt
|
|
69
|
+
```
|
|
70
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ModForge-CLI ⛏
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://docs.modrinth.com/api-spec)
|
|
6
|
+
|
|
7
|
+
**ModForge-CLI** is a powerful CLI tool for building and managing custom Minecraft modpacks using the Modrinth API v2.
|
|
8
|
+
|
|
9
|
+
Search for projects, fetch versions, validate manifests, download mods with hash checks, and generate complete files — all from the terminal.
|
|
10
|
+
|
|
11
|
+
Ideal for modpack developers, server admins, and automation scripts.
|
|
12
|
+
|
|
13
|
+
## Terminal Banner
|
|
14
|
+
|
|
15
|
+
When you run ModForge-CLI, you'll be greeted with this colorful Minecraft-themed banner
|
|
16
|
+
|
|
17
|
+
## Key Features
|
|
18
|
+
|
|
19
|
+
- **Modrinth API v2 Integration**: Search projects, list versions, fetch metadata in bulk.
|
|
20
|
+
- **Modpack Management**: Read/validate `modrinth.index.json`, build packs from metadata.
|
|
21
|
+
- **Validation**: Full JSON Schema checks + optional Pydantic models for strict typing.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Requires **Python 3.13+**.
|
|
26
|
+
|
|
27
|
+
**Recommended (Poetry)**:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
poetry install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Alternative (pip)**:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -r requirements.txt
|
|
37
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "modforge-cli"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
description = "ModForge-CLI — a Modrinth-based Minecraft modpack builder"
|
|
5
|
+
authors = [{ name = "Frank1o3", email = "jahdy1o3@gmail.com" }]
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10,<4.0"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"requests (>=2.32.5,<3.0.0)",
|
|
11
|
+
"jsonschema (>=4.25.1,<5.0.0)",
|
|
12
|
+
"aiofiles (>=25.1.0,<26.0.0)",
|
|
13
|
+
"pydantic (>=2.12.5,<3.0.0)",
|
|
14
|
+
"pyzipper (>=0.3.6,<0.4.0)",
|
|
15
|
+
"rich (>=14.2.0,<15.0.0)",
|
|
16
|
+
"aiohttp (>=3.13.3,<4.0.0)",
|
|
17
|
+
"pyfiglet (>=1.0.4,<2.0.0)",
|
|
18
|
+
"typer (>=0.21.1,<0.22.0)",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 4 - Beta",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Topic :: Software Development :: Build Tools",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
35
|
+
build-backend = "poetry.core.masonry.api"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"types-requests (>=2.32.4.20250913,<3.0.0.0)",
|
|
40
|
+
"types-jsonschema (>=4.25.1.20251009,<5.0.0.0)",
|
|
41
|
+
"types-tqdm (>=4.67.0.20250809,<5.0.0.0)",
|
|
42
|
+
"ruff (>=0.14.8,<0.15.0)",
|
|
43
|
+
"mypy (>=1.19.0,<2.0.0)",
|
|
44
|
+
"types-colorama (>=0.4.15.20250801,<0.5.0.0)",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.poetry]
|
|
48
|
+
package-mode = true
|
|
49
|
+
|
|
50
|
+
[tool.poetry.scripts]
|
|
51
|
+
modforge = "modforge_cli.__main__:main"
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://frank1o3.github.io/ModForge-CLI/"
|
|
55
|
+
Repository = "https://github.com/Frank1o3/ModForge-CLI"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
api/modrith_api.py - Modrinth API v2 URL builder using modrinth_api.json
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
from urllib.parse import quote_plus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModrinthAPIConfig:
|
|
14
|
+
"""Loads modrinth_api.json and builds Modrinth API URLs."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config_path: str | Path = "configs/modrinth_api.json"):
|
|
17
|
+
self.config_path = config_path if isinstance(config_path, Path) else Path(config_path)
|
|
18
|
+
self.base_url: str = ""
|
|
19
|
+
self.endpoints: Dict[str, Any] = {}
|
|
20
|
+
self._load_config()
|
|
21
|
+
|
|
22
|
+
def _load_config(self) -> None:
|
|
23
|
+
if not self.config_path.exists():
|
|
24
|
+
raise FileNotFoundError(
|
|
25
|
+
f"Modrinth API config not found: {self.config_path}"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
29
|
+
data = json.load(f)
|
|
30
|
+
|
|
31
|
+
self.base_url = data.get("BASE_URL", "").rstrip("/")
|
|
32
|
+
if not self.base_url:
|
|
33
|
+
raise ValueError("BASE_URL missing in modrinth_api.json")
|
|
34
|
+
|
|
35
|
+
self.endpoints = data.get("ENDPOINTS", {})
|
|
36
|
+
if not isinstance(self.endpoints, Dict):
|
|
37
|
+
raise ValueError("ENDPOINTS section is invalid")
|
|
38
|
+
|
|
39
|
+
def build_url(self, template: str, **kwargs: str) -> str:
|
|
40
|
+
"""Format a template string with kwargs and prepend base URL."""
|
|
41
|
+
try:
|
|
42
|
+
path = template.format(**kwargs)
|
|
43
|
+
return f"{self.base_url}{path}"
|
|
44
|
+
except KeyError as e:
|
|
45
|
+
raise ValueError(f"Missing URL parameter: {e}")
|
|
46
|
+
|
|
47
|
+
# === Search ===
|
|
48
|
+
|
|
49
|
+
def search(
|
|
50
|
+
self,
|
|
51
|
+
query: Optional[str] = None,
|
|
52
|
+
facets: Optional[List[List[str]] | str] = None,
|
|
53
|
+
categories: Optional[List[str]] = None,
|
|
54
|
+
loaders: Optional[List[str]] = None,
|
|
55
|
+
game_versions: Optional[List[str]] = None,
|
|
56
|
+
license_: Optional[str] = None,
|
|
57
|
+
project_type: Optional[str] = None,
|
|
58
|
+
offset: Optional[int] = None,
|
|
59
|
+
limit: Optional[int] = 10,
|
|
60
|
+
index: Optional[str] = "relevance",
|
|
61
|
+
) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Build the Modrinth search URL with query parameters.
|
|
64
|
+
|
|
65
|
+
Docs: https://docs.modrinth.com/api-spec#endpoints-search
|
|
66
|
+
|
|
67
|
+
Facets format: [[inner AND], [inner AND]] = outer OR
|
|
68
|
+
Example: [["categories:performance"], ["project_type:mod"]]
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
query: Search term (e.g., "sodium")
|
|
72
|
+
facets: Advanced filters as list of lists or JSON string
|
|
73
|
+
categories: Filter by categories (e.g., ["performance"])
|
|
74
|
+
loaders: Filter by loaders (e.g., ["fabric", "quilt"])
|
|
75
|
+
game_versions: Filter by Minecraft versions (e.g., ["1.21.1"])
|
|
76
|
+
license_: Filter by license (e.g., "MIT")
|
|
77
|
+
project_type: "mod", "resourcepack", "shader", "modpack", "datapack"
|
|
78
|
+
offset: Pagination offset
|
|
79
|
+
limit: Results per page (max 100)
|
|
80
|
+
index: Sort by "relevance", "downloads", "updated", "newest"
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Full search URL with query parameters
|
|
84
|
+
"""
|
|
85
|
+
base = self.build_url(self.endpoints["search"])
|
|
86
|
+
params = []
|
|
87
|
+
if query:
|
|
88
|
+
params.append(f"query={quote_plus(query)}")
|
|
89
|
+
|
|
90
|
+
facets_array = []
|
|
91
|
+
if facets:
|
|
92
|
+
if isinstance(facets, str):
|
|
93
|
+
params.append(f"facets={quote_plus(facets)}")
|
|
94
|
+
else:
|
|
95
|
+
facets_array.extend(facets)
|
|
96
|
+
|
|
97
|
+
if project_type:
|
|
98
|
+
facets_array.append([f"project_type:{project_type}"])
|
|
99
|
+
if categories:
|
|
100
|
+
[facets_array.append([f"categories:{c}"]) for c in categories]
|
|
101
|
+
if loaders:
|
|
102
|
+
facets_array.append([f"categories:{l}" for l in loaders])
|
|
103
|
+
if game_versions:
|
|
104
|
+
facets_array.append([f"versions:{v}" for v in game_versions])
|
|
105
|
+
if license_:
|
|
106
|
+
facets_array.append([f"license:{license_}"])
|
|
107
|
+
|
|
108
|
+
if facets_array and not (isinstance(facets, str)):
|
|
109
|
+
params.append(f"facets={quote_plus(json.dumps(facets_array))}")
|
|
110
|
+
|
|
111
|
+
if offset is not None:
|
|
112
|
+
params.append(f"offset={offset}")
|
|
113
|
+
if limit is not None:
|
|
114
|
+
params.append(f"limit={min(limit, 100)}")
|
|
115
|
+
if index:
|
|
116
|
+
params.append(f"index={index}")
|
|
117
|
+
|
|
118
|
+
return f"{base}?{'&'.join(params)}" if params else base
|
|
119
|
+
|
|
120
|
+
# === Projects ===
|
|
121
|
+
|
|
122
|
+
def project(self, project_id: str) -> str:
|
|
123
|
+
return self.build_url(self.endpoints["projects"]["project"], id=project_id)
|
|
124
|
+
|
|
125
|
+
def project_versions(self, project_id: str) -> str:
|
|
126
|
+
return self.build_url(
|
|
127
|
+
self.endpoints["projects"]["project_versions"], id=project_id
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def project_dependencies(self, project_id: str) -> str:
|
|
131
|
+
return self.build_url(self.endpoints["projects"]["dependencies"], id=project_id)
|
|
132
|
+
|
|
133
|
+
def project_gallery(self, project_id: str) -> str:
|
|
134
|
+
return self.build_url(self.endpoints["projects"]["gallery"], id=project_id)
|
|
135
|
+
|
|
136
|
+
def project_icon(self, project_id: str) -> str:
|
|
137
|
+
return self.build_url(self.endpoints["projects"]["icon"], id=project_id)
|
|
138
|
+
|
|
139
|
+
def check_following(self, project_id: str) -> str:
|
|
140
|
+
return self.build_url(
|
|
141
|
+
self.endpoints["projects"]["check_following"], id=project_id
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# === Versions ===
|
|
145
|
+
|
|
146
|
+
def version(self, version_id: str) -> str:
|
|
147
|
+
return self.build_url(self.endpoints["versions"]["version"], id=version_id)
|
|
148
|
+
|
|
149
|
+
def version_files(self, version_id: str) -> str:
|
|
150
|
+
return self.build_url(self.endpoints["versions"]["files"], id=version_id)
|
|
151
|
+
|
|
152
|
+
def version_file_download(self, version_id: str, filename: str) -> str:
|
|
153
|
+
return self.build_url(
|
|
154
|
+
self.endpoints["versions"]["download"], id=version_id, filename=filename
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def file_by_hash(self, hash_: str) -> str:
|
|
158
|
+
return self.build_url(self.endpoints["versions"]["file_by_hash"], hash=hash_)
|
|
159
|
+
|
|
160
|
+
def versions_by_hash(self, hash_: str) -> str:
|
|
161
|
+
return self.build_url(
|
|
162
|
+
self.endpoints["versions"]["versions_by_hash"], hash=hash_
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def latest_version_for_hash(self, hash_: str, algorithm: str = "sha1") -> str:
|
|
166
|
+
return self.build_url(
|
|
167
|
+
self.endpoints["versions"]["latest_for_hash"], hash=hash_, algo=algorithm
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# === Tags ===
|
|
171
|
+
|
|
172
|
+
def categories(self) -> str:
|
|
173
|
+
return self.build_url(self.endpoints["tags"]["categories"])
|
|
174
|
+
|
|
175
|
+
def loaders(self) -> str:
|
|
176
|
+
return self.build_url(self.endpoints["tags"]["loaders"])
|
|
177
|
+
|
|
178
|
+
def game_versions(self) -> str:
|
|
179
|
+
return self.build_url(self.endpoints["tags"]["game_versions"])
|
|
180
|
+
|
|
181
|
+
def licenses(self) -> str:
|
|
182
|
+
return self.build_url(self.endpoints["tags"]["licenses"])
|
|
183
|
+
|
|
184
|
+
def environments(self) -> str:
|
|
185
|
+
return self.build_url(self.endpoints["tags"]["environments"])
|
|
186
|
+
|
|
187
|
+
# === Teams ===
|
|
188
|
+
|
|
189
|
+
def team(self, team_id: str) -> str:
|
|
190
|
+
return self.build_url(self.endpoints["teams"]["team"], id=team_id)
|
|
191
|
+
|
|
192
|
+
def team_members(self, team_id: str) -> str:
|
|
193
|
+
return self.build_url(self.endpoints["teams"]["members"], id=team_id)
|
|
194
|
+
|
|
195
|
+
# === User ===
|
|
196
|
+
|
|
197
|
+
def user(self, user_id: str) -> str:
|
|
198
|
+
return self.build_url(self.endpoints["user"]["user"], id=user_id)
|
|
199
|
+
|
|
200
|
+
def user_projects(self, user_id: str) -> str:
|
|
201
|
+
return self.build_url(self.endpoints["user"]["user_projects"], id=user_id)
|
|
202
|
+
|
|
203
|
+
def user_notifications(self, user_id: str) -> str:
|
|
204
|
+
return self.build_url(self.endpoints["user"]["notifications"], id=user_id)
|
|
205
|
+
|
|
206
|
+
def user_avatar(self, user_id: str) -> str:
|
|
207
|
+
return self.build_url(self.endpoints["user"]["avatar"], id=user_id)
|
|
208
|
+
|
|
209
|
+
# === Bulk ===
|
|
210
|
+
|
|
211
|
+
def bulk_projects(self) -> str:
|
|
212
|
+
return self.build_url(self.endpoints["bulk"]["projects"])
|
|
213
|
+
|
|
214
|
+
def bulk_versions(self) -> str:
|
|
215
|
+
return self.build_url(self.endpoints["bulk"]["versions"])
|
|
216
|
+
|
|
217
|
+
def bulk_version_files(self) -> str:
|
|
218
|
+
return self.build_url(self.endpoints["bulk"]["version_files"])
|