runspec-gitlab-core 0.3.0__tar.gz → 0.5.0__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.
@@ -59,3 +59,6 @@ chainlit-docs/
59
59
 
60
60
  # Stray committed test venv (removed from tracking)
61
61
  .venv-test/
62
+
63
+ # Private gitleaks rules (see .gitleaks.toml)
64
+ .gitleaks.local.toml
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to `runspec-gitlab-core` are documented here.
4
4
 
5
+ ## [0.5.0] — 2026-10-25
6
+
7
+ - `search_projects` can list the token user's projects: `search` is now
8
+ optional, and with no term the call asks for member projects only (listing
9
+ every visible project on gitlab.com is no use). A term also matches the
10
+ group/namespace path (`search_namespaces`), so a username or group name finds
11
+ the projects under it. New `owned` parameter limits results to owned projects.
12
+
13
+ ## [0.4.0] — 2026-10-25
14
+
15
+ - `config_from_env` reads the token from `GITLAB_TOKEN` or `GITLAB_PAT` (each
16
+ also under the `GITLAB_<ENV>_` prefix) — the two names a runspec-console token
17
+ credential labelled `GitLab` sets — and takes `url` / `token` keyword
18
+ overrides that win over the environment.
19
+ - The "No token configured" error also names the console credential route.
20
+
5
21
  ## [0.3.0]
6
22
 
7
23
  Write helpers, each taking a `GitlabConfig` first like the reads.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: runspec-gitlab-core
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Pure-Python GitLab REST helpers — the importable core behind runspec-gitlab (no runspec dependency, no runnables)
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: httpx
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-gitlab-core"
7
- version = "0.3.0"
7
+ version = "0.5.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "Pure-Python GitLab REST helpers — the importable core behind runspec-gitlab (no runspec dependency, no runnables)"
10
10
  dependencies = [
@@ -7,7 +7,7 @@ credentials and call the same functions the CLI runnable's verbs call.
7
7
 
8
8
  Auth is environment-variable driven (see :func:`config_from_env`) but a caller is
9
9
  free to build a :class:`GitlabConfig` by hand. A single personal-access token
10
- (``GITLAB_TOKEN``) is sent as the ``PRIVATE-TOKEN`` header; the instance URL
10
+ (``GITLAB_TOKEN`` or ``GITLAB_PAT``) is sent as the ``PRIVATE-TOKEN`` header; the instance URL
11
11
  (``GITLAB_URL``) defaults to ``https://gitlab.com`` for the SaaS case.
12
12
 
13
13
  This mirrors the read tools of GitLab's own (hosted) MCP server, but ports them
@@ -65,13 +65,16 @@ def _env_first(names: list[str]) -> str | None:
65
65
  return None
66
66
 
67
67
 
68
- def config_from_env(env: str | None = None) -> GitlabConfig:
68
+ def config_from_env(env: str | None = None, *, url: str | None = None, token: str | None = None) -> GitlabConfig:
69
69
  """Build a :class:`GitlabConfig` from environment variables.
70
70
 
71
71
  When ``env`` (or the ``GITLAB_ENV`` variable) selects dev/uat/prod, the
72
72
  environment-prefixed variables are read first (e.g. ``GITLAB_UAT_TOKEN``),
73
73
  falling back to the unprefixed ``GITLAB_*`` family for single-instance
74
- setups. ``GITLAB_URL`` defaults to ``https://gitlab.com`` when unset. Raises
74
+ setups. The token is read from ``GITLAB_TOKEN`` or ``GITLAB_PAT`` (the two
75
+ names a runspec-console token credential labelled ``GitLab`` sets).
76
+ ``GITLAB_URL`` defaults to ``https://gitlab.com`` when unset. Explicit
77
+ ``url`` / ``token`` arguments win over the environment. Raises
75
78
  :class:`GitlabError` with an actionable message when the env label is invalid
76
79
  or the token is missing.
77
80
  """
@@ -81,19 +84,19 @@ def config_from_env(env: str | None = None) -> GitlabConfig:
81
84
  if env not in VALID_ENVS:
82
85
  raise GitlabError(f"Invalid env {env!r} — expected one of {', '.join(VALID_ENVS)}")
83
86
 
84
- def lookup(suffix: str) -> str | None:
87
+ def lookup(*suffixes: str) -> str | None:
85
88
  names = []
86
89
  if env:
87
- names.append(f"GITLAB_{env.upper()}_{suffix}")
88
- names.append(f"GITLAB_{suffix}")
90
+ names += [f"GITLAB_{env.upper()}_{suffix}" for suffix in suffixes]
91
+ names += [f"GITLAB_{suffix}" for suffix in suffixes]
89
92
  return _env_first(names)
90
93
 
91
- token = lookup("TOKEN")
94
+ token = token or lookup("TOKEN", "PAT")
92
95
  if not token:
93
96
  hint = f"GITLAB_{env.upper()}_TOKEN" if env else "GITLAB_TOKEN"
94
- raise GitlabError(f"No token configured — set {hint} (a GitLab personal access token)")
97
+ raise GitlabError(f"No token configured — set {hint} (a GitLab personal access token), or add a runspec-console token credential labelled GitLab")
95
98
 
96
- base_url = lookup("URL") or DEFAULT_URL
99
+ base_url = url or lookup("URL") or DEFAULT_URL
97
100
 
98
101
  return GitlabConfig(
99
102
  base_url=base_url.rstrip("/"),
@@ -47,27 +47,32 @@ def get_project(config: GitlabConfig, project: str | None = None) -> dict[str, A
47
47
 
48
48
  def search_projects(
49
49
  config: GitlabConfig,
50
- search: str,
50
+ search: str | None = None,
51
51
  membership: bool = False,
52
52
  per_page: int = 20,
53
53
  page: int = 1,
54
+ owned: bool = False,
54
55
  ) -> list[dict[str, Any]]:
55
- """Search visible projects by name/path.
56
-
57
- ``membership`` limits results to projects the token's user is a member of.
56
+ """Search or list projects, most recently active first.
57
+
58
+ ``search`` matches the project name or path, including its group/namespace
59
+ path (so ``"finejason"`` finds projects under that user's namespace). GitLab
60
+ matches a term shorter than 3 characters only as an exact name. With no
61
+ ``search`` the call lists the token user's projects: ``membership`` is
62
+ implied, since listing every visible project on a public instance is
63
+ useless. ``owned`` limits results to projects the user owns.
58
64
  """
59
- return gitlab_request(
60
- "GET",
61
- "/projects",
62
- config=config,
63
- params={
64
- "search": search,
65
- "membership": membership,
66
- "order_by": "last_activity_at",
67
- "per_page": per_page,
68
- "page": page,
69
- },
70
- )
65
+ params: dict[str, Any] = {
66
+ "membership": membership or not search or None,
67
+ "owned": owned or None,
68
+ "order_by": "last_activity_at",
69
+ "per_page": per_page,
70
+ "page": page,
71
+ }
72
+ if search:
73
+ params["search"] = search
74
+ params["search_namespaces"] = True
75
+ return gitlab_request("GET", "/projects", config=config, params=params)
71
76
 
72
77
 
73
78
  def resolve_user_ids(config: GitlabConfig, usernames: list[str] | None) -> list[int] | None:
@@ -9,7 +9,7 @@ import pytest
9
9
 
10
10
  from runspec_gitlab_core import client as client_mod
11
11
 
12
- _SUFFIXES = ("URL", "TOKEN", "PROJECT")
12
+ _SUFFIXES = ("URL", "TOKEN", "PAT", "PROJECT")
13
13
  _ALL_VARS = ["GITLAB_ENV"] + [f"GITLAB_{s}" for s in _SUFFIXES]
14
14
  for _env in ("DEV", "UAT", "PROD"):
15
15
  _ALL_VARS += [f"GITLAB_{_env}_{s}" for s in _SUFFIXES]
@@ -53,6 +53,23 @@ def test_search_projects_encodes_membership(monkeypatch):
53
53
  assert seen["path"] == "/api/v4/projects"
54
54
  assert seen["query"]["search"] == "runspec"
55
55
  assert seen["query"]["membership"] == "true"
56
+ assert seen["query"]["search_namespaces"] == "true"
57
+ assert "owned" not in seen["query"]
58
+
59
+
60
+ def test_search_projects_without_term_lists_member_projects(monkeypatch):
61
+ seen = _capture(monkeypatch, httpx.Response(200, json=[]))
62
+ search_projects(CFG)
63
+ assert seen["query"]["membership"] == "true"
64
+ assert "search" not in seen["query"]
65
+ assert "search_namespaces" not in seen["query"]
66
+
67
+
68
+ def test_search_projects_term_without_membership_leaves_it_off(monkeypatch):
69
+ seen = _capture(monkeypatch, httpx.Response(200, json=[]))
70
+ search_projects(CFG, "runspec", owned=True)
71
+ assert "membership" not in seen["query"]
72
+ assert seen["query"]["owned"] == "true"
56
73
 
57
74
 
58
75
  def test_list_issues_uses_default_project_and_filters(monkeypatch):
@@ -68,6 +68,30 @@ def test_missing_token_raises(monkeypatch):
68
68
  # ── encode_project ──────────────────────────────────────────────────────────
69
69
 
70
70
 
71
+ def test_token_read_from_gitlab_pat(monkeypatch):
72
+ # A runspec-console token credential labelled "GitLab" sets GITLAB_TOKEN and GITLAB_PAT.
73
+ monkeypatch.setenv("GITLAB_PAT", "pat-tok")
74
+ assert config_from_env().token == "pat-tok"
75
+
76
+
77
+ def test_env_prefixed_pat_beats_unprefixed_token(monkeypatch):
78
+ monkeypatch.setenv("GITLAB_TOKEN", "shared")
79
+ monkeypatch.setenv("GITLAB_UAT_PAT", "uat-pat")
80
+ assert config_from_env(env="uat").token == "uat-pat"
81
+
82
+
83
+ def test_explicit_url_and_token_win(monkeypatch):
84
+ monkeypatch.setenv("GITLAB_TOKEN", "env-tok")
85
+ monkeypatch.setenv("GITLAB_URL", "https://env.example.com")
86
+ cfg = config_from_env(url="https://cli.example.com/", token="cli-tok")
87
+ assert (cfg.base_url, cfg.token) == ("https://cli.example.com", "cli-tok")
88
+
89
+
90
+ def test_missing_token_names_the_console_credential(monkeypatch):
91
+ with pytest.raises(GitlabError, match="credential labelled GitLab"):
92
+ config_from_env()
93
+
94
+
71
95
  def test_encode_project_path():
72
96
  assert encode_project("group/sub/app") == "group%2Fsub%2Fapp"
73
97