bluesurf-mcp 0.1.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.
@@ -0,0 +1,40 @@
1
+ .dev
2
+
3
+ # Secretos / credenciales (crítico: este proyecto maneja auth y cookies de Surf)
4
+ .env
5
+ .env.*
6
+ *.env
7
+ secrets.*
8
+ *.secret
9
+ cookies.txt
10
+ *.cookies
11
+ session.json
12
+ credentials.json
13
+
14
+ # Python
15
+ __pycache__/
16
+ *.py[cod]
17
+ *.egg-info/
18
+ build/
19
+ dist/
20
+ .eggs/
21
+
22
+ # Entornos virtuales
23
+ .venv/
24
+ venv/
25
+ env/
26
+
27
+ # Tests / herramientas
28
+ .pytest_cache/
29
+ .mypy_cache/
30
+ .ruff_cache/
31
+ .coverage
32
+ htmlcov/
33
+
34
+ # IDE
35
+ .vscode/
36
+ .idea/
37
+
38
+ # OS (Windows)
39
+ Thumbs.db
40
+ Desktop.ini
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BluePeople
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,135 @@
1
+ Metadata-Version: 2.5
2
+ Name: bluesurf-mcp
3
+ Version: 0.1.0
4
+ Summary: Servidor MCP local de solo lectura para leer tickets de BlueSurf
5
+ Author: BluePeople
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 BluePeople
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: bluesurf,mcp,tickets
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: httpx>=0.27
31
+ Requires-Dist: keyring>=25
32
+ Requires-Dist: mcp>=2.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
35
+ Requires-Dist: pytest>=8.0; extra == 'dev'
36
+ Requires-Dist: python-dotenv>=1.0; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # bluesurf-mcp
40
+
41
+ Servidor MCP local de **solo lectura** para que Claude u otros agentes IA lean
42
+ tickets de **BlueSurf**. Un solo paquete PyPI incluye el núcleo
43
+ (`bluesurf_client`) y el adaptador MCP (`bluesurf_mcp`).
44
+
45
+ **Solo lectura por diseño:** no hay operaciones de escritura.
46
+
47
+ ## Instalación
48
+
49
+ ```bash
50
+ pip install bluesurf-mcp
51
+ ```
52
+
53
+ En este monorepo, modo editable:
54
+
55
+ ```bash
56
+ pip install -e ./bluesurf-mcp
57
+ ```
58
+
59
+ ## Guardar credenciales (`login` / `logout`)
60
+
61
+ La contraseña no tiene que ir en `mcp.json`. Una vez, en una terminal:
62
+
63
+ ```bash
64
+ bluesurf-mcp login
65
+ ```
66
+
67
+ Pide usuario y contraseña, valida contra BlueSurf y guarda la contraseña en el
68
+ almacén del sistema (Credential Manager en Windows, Keychain en macOS, Secret
69
+ Service en Linux). El username queda en un JSON local, sin la contraseña.
70
+
71
+ ```bash
72
+ bluesurf-mcp logout
73
+ ```
74
+
75
+ borra lo guardado.
76
+
77
+ ## Configuración (variables de entorno)
78
+
79
+ El entorno **gana** sobre lo guardado con `login`. Útil para CI o para
80
+ sobrescribir sin tocar el almacén.
81
+
82
+ Claves canónicas `BLUESURF_*`; se aceptan `SURF_*` como fallback.
83
+
84
+ - `BLUESURF_BASE_URL` — URL base (opcional; default `https://surf.bluepeople.com`).
85
+ - `BLUESURF_COOKIE` — cookie de sesión ya válida → usa `CookieAuth`.
86
+ - `BLUESURF_USERNAME` + `BLUESURF_PASSWORD` — credenciales → usa `CredentialsAuth`.
87
+
88
+ Precedencia: cookie en env → usuario/contraseña en env → `bluesurf-mcp login`.
89
+
90
+ El núcleo (`bluesurf_client`) no persiste secretos.
91
+
92
+ ## Correr el servidor
93
+
94
+ ```bash
95
+ bluesurf-mcp
96
+ ```
97
+
98
+ Se comunica por **stdio** (el transporte estándar para MCP local). Sin
99
+ argumentos no pide contraseña: usa env o las credenciales de `login`.
100
+
101
+ ## Registrar el MCP en un cliente
102
+
103
+ ### Claude Code / Cursor (`.mcp.json` o config de usuario)
104
+
105
+ Tras `bluesurf-mcp login`:
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "bluesurf": {
111
+ "command": "bluesurf-mcp"
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ Si `bluesurf-mcp` no está en el `PATH`, usa la ruta absoluta al ejecutable del
118
+ venv o `python -m bluesurf_mcp`.
119
+
120
+ Las variables `BLUESURF_*` en `env` siguen siendo válidas como override; no hace
121
+ falta poner la contraseña ahí.
122
+
123
+ ## Tools expuestas (solo lectura)
124
+
125
+ - `mis_proyectos` — proyectos del usuario (id, smallCode, code, name).
126
+ - `buscar_mis_tickets(project, include_closed=False)` — tickets asignados al usuario.
127
+ - `obtener_ticket(code)` — detalle de un ticket por su código (p. ej. `PROJ-123`).
128
+
129
+ ## Usar el cliente desde Python
130
+
131
+ El paquete de import del núcleo es `bluesurf_client`:
132
+
133
+ ```python
134
+ from bluesurf_client import BlueSurfClient, CredentialsAuth # o CookieAuth
135
+ ```
@@ -0,0 +1,97 @@
1
+ # bluesurf-mcp
2
+
3
+ Servidor MCP local de **solo lectura** para que Claude u otros agentes IA lean
4
+ tickets de **BlueSurf**. Un solo paquete PyPI incluye el núcleo
5
+ (`bluesurf_client`) y el adaptador MCP (`bluesurf_mcp`).
6
+
7
+ **Solo lectura por diseño:** no hay operaciones de escritura.
8
+
9
+ ## Instalación
10
+
11
+ ```bash
12
+ pip install bluesurf-mcp
13
+ ```
14
+
15
+ En este monorepo, modo editable:
16
+
17
+ ```bash
18
+ pip install -e ./bluesurf-mcp
19
+ ```
20
+
21
+ ## Guardar credenciales (`login` / `logout`)
22
+
23
+ La contraseña no tiene que ir en `mcp.json`. Una vez, en una terminal:
24
+
25
+ ```bash
26
+ bluesurf-mcp login
27
+ ```
28
+
29
+ Pide usuario y contraseña, valida contra BlueSurf y guarda la contraseña en el
30
+ almacén del sistema (Credential Manager en Windows, Keychain en macOS, Secret
31
+ Service en Linux). El username queda en un JSON local, sin la contraseña.
32
+
33
+ ```bash
34
+ bluesurf-mcp logout
35
+ ```
36
+
37
+ borra lo guardado.
38
+
39
+ ## Configuración (variables de entorno)
40
+
41
+ El entorno **gana** sobre lo guardado con `login`. Útil para CI o para
42
+ sobrescribir sin tocar el almacén.
43
+
44
+ Claves canónicas `BLUESURF_*`; se aceptan `SURF_*` como fallback.
45
+
46
+ - `BLUESURF_BASE_URL` — URL base (opcional; default `https://surf.bluepeople.com`).
47
+ - `BLUESURF_COOKIE` — cookie de sesión ya válida → usa `CookieAuth`.
48
+ - `BLUESURF_USERNAME` + `BLUESURF_PASSWORD` — credenciales → usa `CredentialsAuth`.
49
+
50
+ Precedencia: cookie en env → usuario/contraseña en env → `bluesurf-mcp login`.
51
+
52
+ El núcleo (`bluesurf_client`) no persiste secretos.
53
+
54
+ ## Correr el servidor
55
+
56
+ ```bash
57
+ bluesurf-mcp
58
+ ```
59
+
60
+ Se comunica por **stdio** (el transporte estándar para MCP local). Sin
61
+ argumentos no pide contraseña: usa env o las credenciales de `login`.
62
+
63
+ ## Registrar el MCP en un cliente
64
+
65
+ ### Claude Code / Cursor (`.mcp.json` o config de usuario)
66
+
67
+ Tras `bluesurf-mcp login`:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "bluesurf": {
73
+ "command": "bluesurf-mcp"
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Si `bluesurf-mcp` no está en el `PATH`, usa la ruta absoluta al ejecutable del
80
+ venv o `python -m bluesurf_mcp`.
81
+
82
+ Las variables `BLUESURF_*` en `env` siguen siendo válidas como override; no hace
83
+ falta poner la contraseña ahí.
84
+
85
+ ## Tools expuestas (solo lectura)
86
+
87
+ - `mis_proyectos` — proyectos del usuario (id, smallCode, code, name).
88
+ - `buscar_mis_tickets(project, include_closed=False)` — tickets asignados al usuario.
89
+ - `obtener_ticket(code)` — detalle de un ticket por su código (p. ej. `PROJ-123`).
90
+
91
+ ## Usar el cliente desde Python
92
+
93
+ El paquete de import del núcleo es `bluesurf_client`:
94
+
95
+ ```python
96
+ from bluesurf_client import BlueSurfClient, CredentialsAuth # o CookieAuth
97
+ ```
@@ -0,0 +1,26 @@
1
+ """Núcleo de solo lectura para hablar con BlueSurf.
2
+
3
+ Este paquete concentra toda la lógica reutilizable: autenticación (pluggable),
4
+ manejo de sesión/cookie, endpoints de lectura y modelos tipados. Los adaptadores
5
+ (el servidor MCP local y una eventual CLI) se construyen encima de esto sin
6
+ duplicar lógica.
7
+
8
+ Diseño de solo lectura: aquí no se expone ninguna operación de escritura.
9
+ """
10
+
11
+ from bluesurf_client.auth import AuthError, AuthProvider, CookieAuth, CredentialsAuth
12
+ from bluesurf_client.constants import DEFAULT_BASE_URL
13
+ from bluesurf_client.client import BlueSurfClient
14
+ from bluesurf_client.models import Comment, Ticket, User
15
+
16
+ __all__ = [
17
+ "BlueSurfClient",
18
+ "Ticket",
19
+ "Comment",
20
+ "User",
21
+ "AuthProvider",
22
+ "AuthError",
23
+ "CredentialsAuth",
24
+ "CookieAuth",
25
+ "DEFAULT_BASE_URL",
26
+ ]
@@ -0,0 +1,15 @@
1
+ """Autenticación pluggable contra BlueSurf.
2
+
3
+ El consumidor decide el mecanismo y entrega credenciales o cookie:
4
+ - :class:`CredentialsAuth` — usuario/contraseña (hace login).
5
+ - :class:`CookieAuth` — cookie de sesión ya válida.
6
+
7
+ La librería no persiste credenciales ni sesión: la cookie vive solo en memoria
8
+ durante el ciclo de vida de la instancia del cliente.
9
+ """
10
+
11
+ from bluesurf_client.auth.base import AuthError, AuthProvider
12
+ from bluesurf_client.auth.cookie import CookieAuth
13
+ from bluesurf_client.auth.credentials import CredentialsAuth
14
+
15
+ __all__ = ["AuthProvider", "AuthError", "CredentialsAuth", "CookieAuth"]
@@ -0,0 +1,48 @@
1
+ """Contrato de autenticación (pluggable).
2
+
3
+ Un ``AuthProvider`` sabe cómo dejar una petición HTTP autenticada contra BlueSurf.
4
+ No asume un mecanismo concreto: una implementación puede hacer login con
5
+ credenciales y capturar la cookie ``.AspNetCore.Identity.Application``, otra
6
+ puede reusar una cookie exportada del navegador. El núcleo solo depende de esta
7
+ interfaz.
8
+
9
+ Notas sobre la sesión de BlueSurf:
10
+ - La sesión viaja por cookie de ASP.NET Core Identity, no por header ``Authorization``.
11
+ - Los flags ``samesite=lax`` y ``httponly`` solo afectan al navegador; un cliente
12
+ backend (httpx) puede almacenar y reenviar la cookie sin problema.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import abc
18
+
19
+ import httpx
20
+
21
+
22
+ class AuthError(RuntimeError):
23
+ """La autenticación falló o la sesión no pudo renovarse."""
24
+
25
+
26
+ class AuthProvider(abc.ABC):
27
+ """Interfaz que provee/renueva una sesión autenticada contra BlueSurf.
28
+
29
+ Las implementaciones concretas se agregan después (login programático,
30
+ cookie del navegador, etc.). El contrato es intencionalmente mínimo.
31
+ """
32
+
33
+ @abc.abstractmethod
34
+ async def apply(self, client: httpx.AsyncClient) -> None:
35
+ """Deja ``client`` autenticado.
36
+
37
+ Típicamente instala la cookie de sesión en ``client.cookies``. Se llama
38
+ una vez al construir el cliente y de nuevo tras ``refresh``. Debe lanzar
39
+ :class:`AuthError` si no puede autenticar.
40
+ """
41
+
42
+ async def refresh(self, client: httpx.AsyncClient) -> None:
43
+ """Renueva la sesión tras un 401.
44
+
45
+ Por defecto reintenta ``apply``. Las implementaciones que necesiten
46
+ re-login explícito (p. ej. volver a POSTear credenciales) lo sobrescriben.
47
+ """
48
+ await self.apply(client)
@@ -0,0 +1,40 @@
1
+ """Autenticación por cookie provista por el consumidor.
2
+
3
+ El consumidor entrega una cookie de sesión ya válida (p. ej. extraída de una
4
+ sesión de navegador autenticada). Esta clase la instala en el cliente httpx. No
5
+ persiste nada y no puede renovar la sesión por sí sola: si expira, es
6
+ responsabilidad del consumidor entregar una cookie fresca.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import httpx
12
+
13
+ from bluesurf_client.auth.base import AuthError, AuthProvider
14
+ from bluesurf_client.constants import IDENTITY_COOKIE
15
+
16
+
17
+ class CookieAuth(AuthProvider):
18
+ """Usa una cookie de sesión provista por el consumidor.
19
+
20
+ Por defecto asume la cookie ``.AspNetCore.Identity.Application``; se puede
21
+ indicar otro nombre si hiciera falta.
22
+ """
23
+
24
+ def __init__(self, cookie_value: str, *, cookie_name: str = IDENTITY_COOKIE) -> None:
25
+ if not cookie_value:
26
+ raise ValueError("cookie_value es obligatorio")
27
+ self._cookie_value = cookie_value
28
+ self._cookie_name = cookie_name
29
+
30
+ async def apply(self, client: httpx.AsyncClient) -> None:
31
+ domain = client.base_url.host
32
+ client.cookies.set(
33
+ self._cookie_name, self._cookie_value, domain=domain, path="/"
34
+ )
35
+
36
+ async def refresh(self, client: httpx.AsyncClient) -> None:
37
+ raise AuthError(
38
+ "La cookie provista expiró o fue rechazada. CookieAuth no puede "
39
+ "renovar la sesión: entrega una cookie fresca (o usa CredentialsAuth)."
40
+ )
@@ -0,0 +1,59 @@
1
+ """Autenticación por credenciales (usuario/contraseña).
2
+
3
+ El consumidor entrega usuario y contraseña; esta clase hace el POST de login y
4
+ deja la cookie de sesión en el cliente httpx (en memoria). **No** persiste nada
5
+ en disco: ni credenciales ni cookie. Las credenciales se mantienen en memoria
6
+ solo para poder re-loguear si la sesión expira (401) durante la vida de la
7
+ instancia.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import httpx
13
+
14
+ from bluesurf_client.auth.base import AuthError, AuthProvider
15
+ from bluesurf_client.constants import IDENTITY_COOKIE, LOGIN_PATH
16
+
17
+
18
+ class CredentialsAuth(AuthProvider):
19
+ """Inicia sesión con usuario/contraseña contra ``POST /api/login``.
20
+
21
+ El endpoint espera JSON ``{"email", "password"}`` (el campo es ``email``
22
+ aunque el valor sea un nombre de usuario). No usa CSRF/anti-forgery.
23
+ """
24
+
25
+ def __init__(self, username: str, password: str) -> None:
26
+ if not username or not password:
27
+ raise ValueError("username y password son obligatorios")
28
+ # Solo en memoria; nunca se escriben a disco.
29
+ self._username = username
30
+ self._password = password
31
+
32
+ async def apply(self, client: httpx.AsyncClient) -> None:
33
+ base = str(client.base_url).rstrip("/")
34
+ try:
35
+ response = await client.post(
36
+ LOGIN_PATH,
37
+ json={"email": self._username, "password": self._password},
38
+ headers={"Origin": base, "Referer": f"{base}/login"},
39
+ )
40
+ except httpx.HTTPError as exc: # pragma: no cover - error de red
41
+ raise AuthError(f"No se pudo contactar el login de BlueSurf: {exc}") from exc
42
+
43
+ if response.status_code in (400, 401, 403):
44
+ raise AuthError(
45
+ f"Login rechazado por BlueSurf (HTTP {response.status_code}): "
46
+ "revisa usuario/contraseña"
47
+ )
48
+ response.raise_for_status()
49
+
50
+ # httpx captura el Set-Cookie automáticamente en client.cookies.
51
+ if IDENTITY_COOKIE not in client.cookies:
52
+ raise AuthError(
53
+ "El login respondió 200 pero no llegó la cookie de sesión "
54
+ f"'{IDENTITY_COOKIE}'"
55
+ )
56
+
57
+ async def refresh(self, client: httpx.AsyncClient) -> None:
58
+ # La cookie expiró: volvemos a loguear con las credenciales en memoria.
59
+ await self.apply(client)
@@ -0,0 +1,145 @@
1
+ """Cliente de solo lectura de BlueSurf (la fachada de alto nivel).
2
+
3
+ Expone las lecturas centradas en el objetivo: los tickets (work items) asignados
4
+ al usuario. En BlueSurf no hay un endpoint "asignados a mí"; se resuelve consultando
5
+ el kanban de un proyecto con el filtro ``assignedTo`` puesto en el id del usuario
6
+ actual.
7
+
8
+ Ciclo de vida:
9
+ - Construcción sin efectos: ``BlueSurfClient(base_url=...)``.
10
+ - Autenticación explícita: ``await client.login(auth)`` (inyección por método).
11
+ - Limpieza: ``await client.aclose()``, o usa ``async with`` que la hace sola.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ from bluesurf_client.auth.base import AuthProvider
19
+ from bluesurf_client.constants import DEFAULT_BASE_URL, IDENTITY_COOKIE
20
+ from bluesurf_client.models import Ticket, User
21
+ from bluesurf_client.session import BlueSurfSession
22
+
23
+ _PATH_CURRENT_USER = "/api/instance/currentUser"
24
+ _PATH_MY_PROJECTS = "/api/user/current/project"
25
+ _PATH_KANBAN = "/api/project/{project}/kanban"
26
+ _PATH_WORKITEM = "/api/workItem/{code}"
27
+
28
+ # Body base del kanban (todos los filtros vacíos). Se rellena ``assignedTo`` con
29
+ # el id del usuario actual para que BlueSurf filtre del lado servidor.
30
+ _KANBAN_FILTER_TEMPLATE: dict[str, Any] = {
31
+ "statuses": [],
32
+ "tags": [],
33
+ "sprints": [],
34
+ "priorities": [],
35
+ "assignedTo": [],
36
+ "reportedBy": [],
37
+ "colors": [],
38
+ "defectsOrigins": [],
39
+ "tagCondition": "and",
40
+ "search": "",
41
+ "quantityPerStatus": [],
42
+ }
43
+
44
+
45
+ class BlueSurfClient:
46
+ """Fachada de solo lectura sobre la API de BlueSurf."""
47
+
48
+ def __init__(self, *, base_url: str = DEFAULT_BASE_URL) -> None:
49
+ self._session = BlueSurfSession(base_url=base_url)
50
+ self._current_user: User | None = None
51
+
52
+ async def __aenter__(self) -> "BlueSurfClient":
53
+ return self
54
+
55
+ async def __aexit__(self, *exc: object) -> None:
56
+ await self._session.aclose()
57
+
58
+ async def login(self, auth: AuthProvider) -> "BlueSurfClient":
59
+ """Autentica la sesión. Devuelve ``self`` para encadenar."""
60
+ await self._session.login(auth)
61
+ return self
62
+
63
+ @property
64
+ def authenticated(self) -> bool:
65
+ return self._session.authenticated
66
+
67
+ def session_cookie(self, name: str = IDENTITY_COOKIE) -> str | None:
68
+ """Valor de la cookie de sesión (por defecto la de ASP.NET Identity)."""
69
+ return self._session.cookie(name)
70
+
71
+ async def aclose(self) -> None:
72
+ await self._session.aclose()
73
+
74
+ async def current_user(self) -> User:
75
+ """El usuario autenticado. Se cachea en la instancia tras la 1ª llamada."""
76
+ if self._current_user is None:
77
+ data = await self._session.get_json(_PATH_CURRENT_USER)
78
+ self._current_user = User.from_current_user(data)
79
+ return self._current_user
80
+
81
+ async def mis_proyectos(self) -> list[dict]:
82
+ """Proyectos del usuario (crudo). Útil para descubrir el ``smallCode``."""
83
+ data = await self._session.get_json(_PATH_MY_PROJECTS)
84
+ return data if isinstance(data, list) else []
85
+
86
+ async def mis_tickets(
87
+ self, project: str, *, include_closed: bool = False
88
+ ) -> list[Ticket]:
89
+ """Tickets asignados al usuario en un proyecto.
90
+
91
+ Args:
92
+ project: ``smallCode`` del proyecto (p. ej. ``"PROJ"``), que es el
93
+ prefijo del código de los tickets (``PROJ-123``).
94
+ include_closed: si ``False`` (default) omite los tickets en estados
95
+ finales (cerrados/done); si ``True`` los incluye.
96
+
97
+ Nota: el kanban pagina por columna. Con el filtro por asignado el volumen
98
+ es bajo, pero si tuvieras muchísimos tickets abiertos en una sola columna
99
+ podrían no venir todos en una sola respuesta.
100
+ """
101
+ me = await self.current_user()
102
+
103
+ body = dict(_KANBAN_FILTER_TEMPLATE)
104
+ body["assignedTo"] = [me.id]
105
+
106
+ data = await self._session.query_json(
107
+ _PATH_KANBAN.format(project=project),
108
+ json=body,
109
+ params={"skipWorkItems": "false"},
110
+ )
111
+
112
+ tickets: list[Ticket] = []
113
+ for raw in _iter_work_items(data):
114
+ ticket = Ticket.from_json(raw)
115
+ # Safety: garantizamos que sean míos aunque el filtro del servidor
116
+ # cambiara de semántica.
117
+ if ticket.assignee is None or ticket.assignee.id != me.id:
118
+ continue
119
+ if not include_closed and ticket.status_is_final:
120
+ continue
121
+ tickets.append(ticket)
122
+ return tickets
123
+
124
+ async def obtener_ticket(self, code: str) -> Ticket:
125
+ """Detalle de un ticket por su código (p. ej. ``"PROJ-123"``)."""
126
+ data = await self._session.get_json(_PATH_WORKITEM.format(code=code))
127
+ return Ticket.from_json(data)
128
+
129
+
130
+ def _iter_work_items(kanban: Any):
131
+ """Aplana la respuesta del kanban → work items.
132
+
133
+ Estructura: lista por tipo → ``statuses[]`` (columnas) → ``workItems[]``.
134
+ """
135
+ if not isinstance(kanban, list):
136
+ return
137
+ for branch in kanban:
138
+ if not isinstance(branch, dict):
139
+ continue
140
+ for status in branch.get("statuses", []):
141
+ if not isinstance(status, dict):
142
+ continue
143
+ for wi in status.get("workItems", []):
144
+ if isinstance(wi, dict):
145
+ yield wi
@@ -0,0 +1,10 @@
1
+ """Constantes del núcleo."""
2
+
3
+ # URL base por defecto de BlueSurf. Sobrescribible al construir el cliente/auth.
4
+ DEFAULT_BASE_URL = "https://surf.bluepeople.com"
5
+
6
+ # Cookie de sesión que emite ASP.NET Core Identity al iniciar sesión.
7
+ IDENTITY_COOKIE = ".AspNetCore.Identity.Application"
8
+
9
+ # Ruta del endpoint de login (POST JSON {"email", "password"}).
10
+ LOGIN_PATH = "/api/login"