forgexa-cli 1.8.4__tar.gz → 1.8.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.
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/PKG-INFO +2 -1
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli/daemon.py +170 -30
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli.egg-info/PKG-INFO +2 -1
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/pyproject.toml +2 -1
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/README.md +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.8.4 → forgexa_cli-1.8.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.5
|
|
4
4
|
Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
|
|
5
5
|
Author-email: Jason Sun <dev.winds@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
24
|
Classifier: Topic :: Software Development :: Build Tools
|
|
24
25
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
26
|
Requires-Python: >=3.9
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.8.
|
|
2
|
+
__version__ = "1.8.5"
|
|
@@ -15,11 +15,14 @@ import sys
|
|
|
15
15
|
# ── Python version gate — must run before any other imports ──────────────────
|
|
16
16
|
# Emit a machine-readable DAEMON_ERROR so the desktop app shows a clear
|
|
17
17
|
# message instead of a cryptic traceback.
|
|
18
|
+
# Minimum: 3.9 (macOS ships 3.9; CLI/daemon run on end-user machines).
|
|
19
|
+
# from __future__ import annotations (line 11) makes all X|Y union hints
|
|
20
|
+
# lazy strings on 3.9/3.10, so no runtime SyntaxError on those versions.
|
|
18
21
|
if sys.version_info < (3, 9):
|
|
19
22
|
_ver = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
20
23
|
print(
|
|
21
|
-
f"DAEMON_ERROR: Python {_ver} is too old. Forgexa Daemon requires Python
|
|
22
|
-
f"newer. Please upgrade Python from https://www.python.org/downloads/",
|
|
24
|
+
f"DAEMON_ERROR: Python {_ver} is too old. Forgexa Daemon requires Python "
|
|
25
|
+
f"3.9 or newer. Please upgrade Python from https://www.python.org/downloads/",
|
|
23
26
|
file=sys.stderr,
|
|
24
27
|
)
|
|
25
28
|
sys.exit(1)
|
|
@@ -82,24 +85,27 @@ def _try_install_httpx(deps_dir: str) -> tuple[bool, str]:
|
|
|
82
85
|
|
|
83
86
|
# Try pip --target first (most universally compatible).
|
|
84
87
|
# Falls back to --user, then --break-system-packages as last resort.
|
|
85
|
-
# We explicitly list httpcore alongside httpx because pip
|
|
86
|
-
# skip transitive deps it finds in system site-packages,
|
|
87
|
-
# they won't be importable from the isolated deps directory.
|
|
88
|
+
# We explicitly list httpcore and certifi alongside httpx because pip
|
|
89
|
+
# --target may skip transitive deps it finds in system site-packages,
|
|
90
|
+
# even though they won't be importable from the isolated deps directory.
|
|
91
|
+
# certifi must be included so that its cacert.pem bundle is copied into
|
|
92
|
+
# the deps dir; without it httpx raises FileNotFoundError when building
|
|
93
|
+
# the default SSL context (observed on Python 3.14 standalone / Windows).
|
|
88
94
|
strategies: list[tuple[str, list[str]]] = [
|
|
89
95
|
(
|
|
90
96
|
"pip install --target (isolated deps)",
|
|
91
97
|
[python, "-m", "pip", "install", "--target", deps_dir,
|
|
92
|
-
"--quiet", "--upgrade", "httpx>=0.24", "httpcore"],
|
|
98
|
+
"--quiet", "--upgrade", "httpx>=0.24", "httpcore", "certifi"],
|
|
93
99
|
),
|
|
94
100
|
(
|
|
95
101
|
"pip install --user",
|
|
96
102
|
[python, "-m", "pip", "install", "--user", "--quiet",
|
|
97
|
-
"httpx>=0.24", "httpcore"],
|
|
103
|
+
"httpx>=0.24", "httpcore", "certifi"],
|
|
98
104
|
),
|
|
99
105
|
(
|
|
100
106
|
"pip install --break-system-packages",
|
|
101
107
|
[python, "-m", "pip", "install", "--quiet",
|
|
102
|
-
"--break-system-packages", "httpx>=0.24", "httpcore"],
|
|
108
|
+
"--break-system-packages", "httpx>=0.24", "httpcore", "certifi"],
|
|
103
109
|
),
|
|
104
110
|
]
|
|
105
111
|
|
|
@@ -190,19 +196,33 @@ def _die_missing_httpx(detail: str) -> None:
|
|
|
190
196
|
def _validate_httpx_imports() -> tuple[bool, str]:
|
|
191
197
|
"""Validate that httpx and its critical transitive deps are importable.
|
|
192
198
|
|
|
193
|
-
A bare ``import httpx`` can succeed even when httpcore is
|
|
194
|
-
because httpx lazily imports its transport
|
|
195
|
-
the full chain so the daemon fails fast with a clear
|
|
196
|
-
of crashing mid-operation
|
|
197
|
-
the transport.
|
|
199
|
+
A bare ``import httpx`` can succeed even when httpcore or certifi is
|
|
200
|
+
missing, because httpx lazily imports its transport and SSL layers.
|
|
201
|
+
We eagerly check the full chain so the daemon fails fast with a clear
|
|
202
|
+
message instead of crashing mid-operation.
|
|
198
203
|
|
|
199
|
-
|
|
204
|
+
Also verifies that certifi's CA bundle file actually exists on disk.
|
|
205
|
+
A ``pip install --target`` run may install certifi's Python package but
|
|
206
|
+
omit the package-data file (cacert.pem) when pip detects certifi in
|
|
207
|
+
the system site-packages and skips re-copying it. Without the bundle,
|
|
208
|
+
httpx raises FileNotFoundError when building the default SSL context
|
|
209
|
+
(observed on Python 3.14 standalone builds on Windows).
|
|
210
|
+
|
|
211
|
+
Returns (ok, missing_module_name). 'certifi:bundle' means certifi is
|
|
212
|
+
importable but its CA bundle file is absent.
|
|
200
213
|
"""
|
|
201
|
-
for mod_name in ("httpx", "httpcore"):
|
|
214
|
+
for mod_name in ("httpx", "httpcore", "certifi"):
|
|
202
215
|
try:
|
|
203
216
|
__import__(mod_name)
|
|
204
217
|
except ImportError:
|
|
205
218
|
return False, mod_name
|
|
219
|
+
# Verify the certifi CA bundle file actually exists on disk
|
|
220
|
+
try:
|
|
221
|
+
import certifi as _certifi_check
|
|
222
|
+
if not os.path.isfile(_certifi_check.where()):
|
|
223
|
+
return False, "certifi:bundle"
|
|
224
|
+
except Exception:
|
|
225
|
+
return False, "certifi"
|
|
206
226
|
return True, ""
|
|
207
227
|
|
|
208
228
|
|
|
@@ -223,8 +243,8 @@ if not _httpx_ok:
|
|
|
223
243
|
if _httpx_missing != "httpx":
|
|
224
244
|
shutil.rmtree(_HTTPX_DEPS_DIR, ignore_errors=True)
|
|
225
245
|
for _mod_key in list(sys.modules):
|
|
226
|
-
if _mod_key in ("httpx", "httpcore") or \
|
|
227
|
-
_mod_key.startswith(("httpx.", "httpcore.")):
|
|
246
|
+
if _mod_key in ("httpx", "httpcore", "certifi") or \
|
|
247
|
+
_mod_key.startswith(("httpx.", "httpcore.", "certifi.")):
|
|
228
248
|
del sys.modules[_mod_key]
|
|
229
249
|
|
|
230
250
|
# Attempt auto-install to user-writable deps directory
|
|
@@ -245,6 +265,46 @@ import httpx # noqa: E402 — guaranteed available after validation above
|
|
|
245
265
|
|
|
246
266
|
del _httpx_ok, _httpx_missing
|
|
247
267
|
|
|
268
|
+
|
|
269
|
+
def _make_httpx_ssl_context():
|
|
270
|
+
"""Build an SSL context resilient to missing or mislocated certifi bundles.
|
|
271
|
+
|
|
272
|
+
On Windows 10/11 with Python 3.13+ / 3.14+, ``ssl.create_default_context()``
|
|
273
|
+
can use the built-in Windows certificate store directly and does NOT need
|
|
274
|
+
an external CA bundle file. However, older httpx versions call
|
|
275
|
+
``certifi.where()`` and pass the resulting path to
|
|
276
|
+
``ssl.create_default_context(cafile=...)``. When running from an
|
|
277
|
+
isolated deps directory, the certifi package may be importable (found
|
|
278
|
+
in system site-packages via sys.path) but its ``cacert.pem`` bundle may
|
|
279
|
+
not exist at the expected path, causing a ``FileNotFoundError``.
|
|
280
|
+
|
|
281
|
+
Strategy (in order):
|
|
282
|
+
1. Explicit certifi bundle — portable; pinned root CAs.
|
|
283
|
+
2. System truststore — works on Windows 11 (cert store), macOS
|
|
284
|
+
(Keychain), and Linux (/etc/ssl). No external file needed.
|
|
285
|
+
3. httpx default (verify=True) — last resort; httpx makes its own
|
|
286
|
+
SSL choice.
|
|
287
|
+
|
|
288
|
+
Returns a value suitable for ``httpx.AsyncClient(verify=...)``.
|
|
289
|
+
"""
|
|
290
|
+
import ssl as _ssl
|
|
291
|
+
# Strategy 1: explicit certifi bundle (most portable)
|
|
292
|
+
try:
|
|
293
|
+
import certifi as _certifi
|
|
294
|
+
cafile = _certifi.where()
|
|
295
|
+
if os.path.isfile(cafile):
|
|
296
|
+
return _ssl.create_default_context(cafile=cafile)
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
# Strategy 2: system truststore (no external file needed)
|
|
300
|
+
try:
|
|
301
|
+
return _ssl.create_default_context()
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
# Strategy 3: let httpx use its own SSL handling
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
|
|
248
308
|
# ── Settings: graceful fallback when running standalone (outside backend package) ──
|
|
249
309
|
try:
|
|
250
310
|
from app.config import settings
|
|
@@ -332,7 +392,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
332
392
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
333
393
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
334
394
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
335
|
-
DAEMON_VERSION = "1.8.
|
|
395
|
+
DAEMON_VERSION = "1.8.5"
|
|
336
396
|
|
|
337
397
|
|
|
338
398
|
def _detect_client_type() -> str:
|
|
@@ -651,7 +711,8 @@ class AgentDiscovery:
|
|
|
651
711
|
},
|
|
652
712
|
"copilot": {
|
|
653
713
|
"commands": ["copilot"],
|
|
654
|
-
"detect": "copilot
|
|
714
|
+
"detect": "copilot version",
|
|
715
|
+
"detect_alt": "copilot --version",
|
|
655
716
|
"invoke_modes": ["cli"],
|
|
656
717
|
"env_path_override": "FACTORY_COPILOT_PATH",
|
|
657
718
|
"compatibility_level": "L3",
|
|
@@ -679,12 +740,30 @@ class AgentDiscovery:
|
|
|
679
740
|
localappdata = Path(os.environ.get("LOCALAPPDATA", home / "AppData" / "Local"))
|
|
680
741
|
extra_dirs += [
|
|
681
742
|
appdata / "npm", # npm -g installs
|
|
682
|
-
localappdata / "Programs" / "Python" / "Scripts",
|
|
683
743
|
home / ".opencode" / "bin",
|
|
684
744
|
home / ".cargo" / "bin",
|
|
685
745
|
home / ".bun" / "bin",
|
|
686
746
|
home / "scoop" / "shims", # scoop package manager
|
|
687
747
|
]
|
|
748
|
+
# Python 3.x on Windows installs Scripts to a versioned subdirectory:
|
|
749
|
+
# %LOCALAPPDATA%\Programs\Python\Python312\Scripts (etc.).
|
|
750
|
+
# Glob all Python3* dirs so we catch whichever version is installed.
|
|
751
|
+
py_base = localappdata / "Programs" / "Python"
|
|
752
|
+
if py_base.is_dir():
|
|
753
|
+
for py_dir in sorted(py_base.glob("Python3*/"), reverse=True):
|
|
754
|
+
scripts = py_dir / "Scripts"
|
|
755
|
+
if scripts.is_dir():
|
|
756
|
+
extra_dirs.append(scripts)
|
|
757
|
+
# Flat path kept for compatibility with non-standard Python installers.
|
|
758
|
+
extra_dirs.append(py_base / "Scripts")
|
|
759
|
+
# GitHub Copilot CLI — VS Code extension installs the binary into
|
|
760
|
+
# AppData\Roaming\Code\User\globalStorage\github.copilot-chat\copilotCli\
|
|
761
|
+
# This dir is NOT in system PATH so the daemon must add it explicitly.
|
|
762
|
+
for vs_variant in ("Code", "Code - Insiders", "VSCodium"):
|
|
763
|
+
extra_dirs.append(
|
|
764
|
+
appdata / vs_variant / "User" / "globalStorage"
|
|
765
|
+
/ "github.copilot-chat" / "copilotCli"
|
|
766
|
+
)
|
|
688
767
|
# nvm-windows stores versions differently
|
|
689
768
|
nvm_home = os.environ.get("NVM_HOME", "")
|
|
690
769
|
if nvm_home:
|
|
@@ -756,6 +835,11 @@ class AgentDiscovery:
|
|
|
756
835
|
resolved = shutil.which(cmd)
|
|
757
836
|
if resolved:
|
|
758
837
|
version = await self._get_version(spec["detect"])
|
|
838
|
+
if version is None and "detect_alt" in spec:
|
|
839
|
+
# Primary detect command failed; try the alternative.
|
|
840
|
+
# Example: new GitHub Copilot CLI 1.0.x uses `copilot version`
|
|
841
|
+
# as a subcommand while older releases used `copilot --version`.
|
|
842
|
+
version = await self._get_version(spec["detect_alt"])
|
|
759
843
|
if version is None:
|
|
760
844
|
# Binary found but version check failed — it is a stub or
|
|
761
845
|
# not properly installed (e.g. copilot prompts to install).
|
|
@@ -785,6 +869,21 @@ class AgentDiscovery:
|
|
|
785
869
|
import re
|
|
786
870
|
try:
|
|
787
871
|
parts = detect_cmd.split()
|
|
872
|
+
# On Windows, agent CLIs installed via npm create .cmd wrapper scripts
|
|
873
|
+
# (e.g. %APPDATA%\npm\copilot.cmd). Python's asyncio.create_subprocess_exec
|
|
874
|
+
# calls CreateProcess which cannot execute .cmd/.bat files directly —
|
|
875
|
+
# they need cmd.exe as the interpreter. Detect and wrap automatically.
|
|
876
|
+
if sys.platform == "win32":
|
|
877
|
+
resolved = shutil.which(parts[0])
|
|
878
|
+
if resolved:
|
|
879
|
+
ext = Path(resolved).suffix.lower()
|
|
880
|
+
if ext in ('.cmd', '.bat'):
|
|
881
|
+
parts = ['cmd', '/c', resolved] + parts[1:]
|
|
882
|
+
elif ext == '.ps1':
|
|
883
|
+
parts = [
|
|
884
|
+
'powershell', '-NoProfile', '-NonInteractive',
|
|
885
|
+
'-Command', resolved,
|
|
886
|
+
] + parts[1:]
|
|
788
887
|
proc = await asyncio.create_subprocess_exec(
|
|
789
888
|
*parts,
|
|
790
889
|
stdin=asyncio.subprocess.DEVNULL,
|
|
@@ -3448,6 +3547,7 @@ class ServerConnection:
|
|
|
3448
3547
|
self.runtime_id: str | None = None
|
|
3449
3548
|
self.client = httpx.AsyncClient(
|
|
3450
3549
|
headers={"Authorization": f"Bearer {api_token}"} if api_token else {},
|
|
3550
|
+
verify=_make_httpx_ssl_context(),
|
|
3451
3551
|
)
|
|
3452
3552
|
self.heartbeat: HeartbeatService | None = None
|
|
3453
3553
|
self.poller: TaskPoller | None = None
|
|
@@ -5824,17 +5924,57 @@ class RuntimeDaemon:
|
|
|
5824
5924
|
)
|
|
5825
5925
|
return f"Push failed: {exc}"
|
|
5826
5926
|
else:
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
f"commit(s) not in local history. Force-pushing would "
|
|
5836
|
-
f"destroy prior implementation work."
|
|
5927
|
+
# Remote has genuinely new commits not in local history.
|
|
5928
|
+
# Before refusing, attempt fetch + rebase: if the local
|
|
5929
|
+
# commits are documentation-only (analysis) or purely
|
|
5930
|
+
# additive they will rebase cleanly onto the remote HEAD.
|
|
5931
|
+
logger.warning(
|
|
5932
|
+
"Branch %s: remote has %d commit(s) not in local history — "
|
|
5933
|
+
"attempting fetch+rebase recovery before refusing push",
|
|
5934
|
+
branch, remote_count,
|
|
5837
5935
|
)
|
|
5936
|
+
try:
|
|
5937
|
+
# 1. Fetch the specific remote branch
|
|
5938
|
+
await git(
|
|
5939
|
+
"fetch", "origin", branch,
|
|
5940
|
+
cwd=workspace_path,
|
|
5941
|
+
)
|
|
5942
|
+
# 2. Rebase local commits onto the updated remote HEAD
|
|
5943
|
+
await git(
|
|
5944
|
+
"-c", "user.name=Forgexa Agent",
|
|
5945
|
+
"-c", "user.email=agent@forgexa.net",
|
|
5946
|
+
"rebase", f"origin/{branch}",
|
|
5947
|
+
cwd=workspace_path,
|
|
5948
|
+
)
|
|
5949
|
+
# 3. Push normally (no force needed — we're ahead of origin now)
|
|
5950
|
+
await git(
|
|
5951
|
+
"push", "-u", "origin", branch,
|
|
5952
|
+
cwd=workspace_path, project_key=project_key,
|
|
5953
|
+
)
|
|
5954
|
+
logger.info(
|
|
5955
|
+
"Fetch+rebase recovery succeeded for branch %s "
|
|
5956
|
+
"(%d remote commit(s) incorporated)",
|
|
5957
|
+
branch, remote_count,
|
|
5958
|
+
)
|
|
5959
|
+
return None
|
|
5960
|
+
except RuntimeError as rebase_exc:
|
|
5961
|
+
rebase_exc_str = str(rebase_exc)
|
|
5962
|
+
# Abort any in-progress rebase to leave workspace clean
|
|
5963
|
+
try:
|
|
5964
|
+
await git("rebase", "--abort", cwd=workspace_path)
|
|
5965
|
+
except RuntimeError:
|
|
5966
|
+
pass
|
|
5967
|
+
logger.error(
|
|
5968
|
+
"Fetch+rebase recovery failed for branch %s: %s — "
|
|
5969
|
+
"refusing push to protect remote work",
|
|
5970
|
+
branch, rebase_exc_str,
|
|
5971
|
+
)
|
|
5972
|
+
return (
|
|
5973
|
+
f"Push refused: remote branch '{branch}' has {remote_count} "
|
|
5974
|
+
f"commit(s) not in local history, and automatic rebase failed "
|
|
5975
|
+
f"(likely a merge conflict). Manual resolution required. "
|
|
5976
|
+
f"Details: {rebase_exc_str[:300]}"
|
|
5977
|
+
)
|
|
5838
5978
|
|
|
5839
5979
|
logger.info("Found unpushed commits on %s, pushing...", branch)
|
|
5840
5980
|
last_push_exc: Exception | None = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forgexa-cli
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.5
|
|
4
4
|
Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
|
|
5
5
|
Author-email: Jason Sun <dev.winds@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
24
|
Classifier: Topic :: Software Development :: Build Tools
|
|
24
25
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
26
|
Requires-Python: >=3.9
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forgexa-cli"
|
|
3
|
-
version = "1.8.
|
|
3
|
+
version = "1.8.5"
|
|
4
4
|
description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
|
|
5
5
|
requires-python = ">=3.9"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.11",
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
24
25
|
"Topic :: Software Development :: Build Tools",
|
|
25
26
|
"Topic :: Software Development :: Quality Assurance",
|
|
26
27
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|