From ba56e26d99603ba0684d58e780fd67bda4bc6128 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Wed, 9 Sep 2026 20:13:00 +0100 Subject: [PATCH 1/2] Add OperatorLogin resource and update version to 2.2.4 --- cuenca/__init__.py | 2 + cuenca/resources/__init__.py | 3 ++ cuenca/resources/operator_logins.py | 66 +++++++++++++++++++++++++ cuenca/version.py | 2 +- tests/resources/test_operator_logins.py | 48 ++++++++++++++++++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 cuenca/resources/operator_logins.py create mode 100644 tests/resources/test_operator_logins.py diff --git a/cuenca/__init__.py b/cuenca/__init__.py index 5ec2c2b9..568f4eb3 100644 --- a/cuenca/__init__.py +++ b/cuenca/__init__.py @@ -25,6 +25,7 @@ 'LimitedWallet', 'LoginToken', 'Otp', + 'OperatorLogin', 'PasswordReset', 'Platform', 'Questionnaires', @@ -80,6 +81,7 @@ KYCValidation, LimitedWallet, LoginToken, + OperatorLogin, Otp, PasswordReset, PhoneVerificationAssociations, diff --git a/cuenca/resources/__init__.py b/cuenca/resources/__init__.py index 1a7a0d05..bbde7b5a 100644 --- a/cuenca/resources/__init__.py +++ b/cuenca/resources/__init__.py @@ -25,6 +25,7 @@ 'LimitedWallet', 'LoginToken', 'Otp', + 'OperatorLogin', 'PasswordReset', 'Platform', 'PhoneVerificationAssociation', @@ -74,6 +75,7 @@ from .kyc_validations import KYCValidation from .limited_wallets import LimitedWallet from .login_tokens import LoginToken +from .operator_logins import OperatorLogin from .otps import Otp from .password_resets import PasswordReset from .phone_verification_associations import PhoneVerificationAssociations @@ -124,6 +126,7 @@ KYCValidation, LimitedWallet, LoginToken, + OperatorLogin, PasswordReset, Questionnaires, Saving, diff --git a/cuenca/resources/operator_logins.py b/cuenca/resources/operator_logins.py new file mode 100644 index 00000000..c71abdea --- /dev/null +++ b/cuenca/resources/operator_logins.py @@ -0,0 +1,66 @@ +from typing import Annotated, Any, ClassVar + +from cuenca_validations.types import LogConfig, OperatorRole +from cuenca_validations.types.requests import OperatorLoginRequest +from pydantic import ConfigDict, model_validator + +from ..http import Session, session as global_session +from .base import Creatable + + +class OperatorLogin(Creatable): + """Authenticate a company operator (portal personas morales). + + Authed endpoint: ``POST /operator-login``. Use ``session_token`` as + ``X-Cuenca-SessionId`` on subsequent requests. + """ + + _resource: ClassVar = 'operator-login' + + # Resource requires ``id``; Authed returns ``session_token`` (Session.id). + id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] + session_token: Annotated[ + str, LogConfig(masked=True, unmasked_chars_length=4) + ] + operator_id: str + role: OperatorRole + legal_person_id: str + + model_config = ConfigDict( + json_schema_extra={ + 'example': { + 'id': 'SEWqY5cvkISJOxHyEKjAKf8w', + 'session_token': 'SEWqY5cvkISJOxHyEKjAKf8w', + 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', + 'role': 'authorizer', + 'legal_person_id': 'USWqY5cvkISJOxHyEKjAKf8w', + } + } + ) + + @model_validator(mode='before') + @classmethod + def populate_id_from_session_token(cls, values: Any) -> Any: + if isinstance(values, dict): + token = values.get('session_token') or values.get('id') + if token: + values['id'] = token + values['session_token'] = token + return values + + @classmethod + def create( + cls, + email: str, + password: str, + *, + session: Session = global_session, + ) -> 'OperatorLogin': + req = OperatorLoginRequest(email=email, password=password) + login = cls._create( + session=session, + email=str(req.email), + password=req.password.get_secret_value(), + ) + session.headers['X-Cuenca-SessionId'] = login.session_token + return login diff --git a/cuenca/version.py b/cuenca/version.py index 67e34901..f189b057 100644 --- a/cuenca/version.py +++ b/cuenca/version.py @@ -1,3 +1,3 @@ -__version__ = '2.2.3' +__version__ = '2.2.4' CLIENT_VERSION = __version__ API_VERSION = '2020-03-19' diff --git a/tests/resources/test_operator_logins.py b/tests/resources/test_operator_logins.py new file mode 100644 index 00000000..24e2eb0c --- /dev/null +++ b/tests/resources/test_operator_logins.py @@ -0,0 +1,48 @@ +from unittest.mock import MagicMock, patch + +import pytest +from cuenca_validations.types import OperatorRole + +from cuenca import OperatorLogin +from cuenca.http.client import Session + + +@pytest.fixture +def session() -> Session: + s = Session() + s.configure('api_key', 'api_secret', sandbox=True) + return s + + +@patch('cuenca.http.client.requests.Session.request') +def test_operator_login_create(mock_request: MagicMock, session: Session): + mock_request.return_value.ok = True + mock_request.return_value.content = ( + b'{"session_token":"SEWqY5cvkISJOxHyEKjAKf8w",' + b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' + b'"role":"operator",' + b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w"}' + ) + + login = OperatorLogin.create( + 'Maria+Tag@Aceros.com', + 'supersecret', + session=session, + ) + + assert login.session_token == 'SEWqY5cvkISJOxHyEKjAKf8w' + assert login.id == login.session_token + assert login.operator_id == 'OPWqY5cvkISJOxHyEKjAKf8w' + assert login.role == OperatorRole.operator + assert login.legal_person_id == 'USWqY5cvkISJOxHyEKjAKf8w' + assert session.headers['X-Cuenca-SessionId'] == login.session_token + assert 'X-Cuenca-LoginId' not in session.headers + + mock_request.assert_called_once() + _, kwargs = mock_request.call_args + assert kwargs['method'] == 'post' + assert kwargs['url'] == 'https://sandbox.cuenca.com/operator-login' + assert kwargs['json'] == { + 'email': 'maria@aceros.com', + 'password': 'supersecret', + } From 4222140a848b6e025601684f2d4c74f61272c646 Mon Sep 17 00:00:00 2001 From: Juliette Trejo Ceballos Date: Wed, 9 Sep 2026 23:31:07 +0100 Subject: [PATCH 2/2] Update cuenca-validations to version 2.1.44 and adjust OperatorLogin resource to use 'id' instead of 'session_token' for session management. --- cuenca/resources/operator_logins.py | 25 +++++-------------------- requirements.txt | 2 +- setup.py | 2 +- tests/resources/test_operator_logins.py | 7 +++---- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/cuenca/resources/operator_logins.py b/cuenca/resources/operator_logins.py index c71abdea..ed0b7c9d 100644 --- a/cuenca/resources/operator_logins.py +++ b/cuenca/resources/operator_logins.py @@ -1,8 +1,8 @@ -from typing import Annotated, Any, ClassVar +from typing import Annotated, ClassVar from cuenca_validations.types import LogConfig, OperatorRole from cuenca_validations.types.requests import OperatorLoginRequest -from pydantic import ConfigDict, model_validator +from pydantic import ConfigDict from ..http import Session, session as global_session from .base import Creatable @@ -11,17 +11,13 @@ class OperatorLogin(Creatable): """Authenticate a company operator (portal personas morales). - Authed endpoint: ``POST /operator-login``. Use ``session_token`` as - ``X-Cuenca-SessionId`` on subsequent requests. + Authed endpoint: ``POST /operator-login``. Response ``id`` is the + Session.id; use it as ``X-Cuenca-SessionId`` on subsequent requests. """ _resource: ClassVar = 'operator-login' - # Resource requires ``id``; Authed returns ``session_token`` (Session.id). id: Annotated[str, LogConfig(masked=True, unmasked_chars_length=4)] - session_token: Annotated[ - str, LogConfig(masked=True, unmasked_chars_length=4) - ] operator_id: str role: OperatorRole legal_person_id: str @@ -30,7 +26,6 @@ class OperatorLogin(Creatable): json_schema_extra={ 'example': { 'id': 'SEWqY5cvkISJOxHyEKjAKf8w', - 'session_token': 'SEWqY5cvkISJOxHyEKjAKf8w', 'operator_id': 'OPWqY5cvkISJOxHyEKjAKf8w', 'role': 'authorizer', 'legal_person_id': 'USWqY5cvkISJOxHyEKjAKf8w', @@ -38,16 +33,6 @@ class OperatorLogin(Creatable): } ) - @model_validator(mode='before') - @classmethod - def populate_id_from_session_token(cls, values: Any) -> Any: - if isinstance(values, dict): - token = values.get('session_token') or values.get('id') - if token: - values['id'] = token - values['session_token'] = token - return values - @classmethod def create( cls, @@ -62,5 +47,5 @@ def create( email=str(req.email), password=req.password.get_secret_value(), ) - session.headers['X-Cuenca-SessionId'] = login.session_token + session.headers['X-Cuenca-SessionId'] = login.id return login diff --git a/requirements.txt b/requirements.txt index 0d18fa50..9530ae79 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests==2.32.3 -cuenca-validations==2.1.39 +cuenca-validations==2.1.44 pydantic-extra-types==2.10.2 diff --git a/setup.py b/setup.py index cf4d610e..c77ce499 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ python_requires='>=3.9', install_requires=[ 'requests>=2.32.0', - 'cuenca-validations>=2.1.34', + 'cuenca-validations>=2.1.44', 'pydantic-extra-types>=2.10.0', ], classifiers=[ diff --git a/tests/resources/test_operator_logins.py b/tests/resources/test_operator_logins.py index 24e2eb0c..02d0353c 100644 --- a/tests/resources/test_operator_logins.py +++ b/tests/resources/test_operator_logins.py @@ -18,7 +18,7 @@ def session() -> Session: def test_operator_login_create(mock_request: MagicMock, session: Session): mock_request.return_value.ok = True mock_request.return_value.content = ( - b'{"session_token":"SEWqY5cvkISJOxHyEKjAKf8w",' + b'{"id":"SEWqY5cvkISJOxHyEKjAKf8w",' b'"operator_id":"OPWqY5cvkISJOxHyEKjAKf8w",' b'"role":"operator",' b'"legal_person_id":"USWqY5cvkISJOxHyEKjAKf8w"}' @@ -30,12 +30,11 @@ def test_operator_login_create(mock_request: MagicMock, session: Session): session=session, ) - assert login.session_token == 'SEWqY5cvkISJOxHyEKjAKf8w' - assert login.id == login.session_token + assert login.id == 'SEWqY5cvkISJOxHyEKjAKf8w' assert login.operator_id == 'OPWqY5cvkISJOxHyEKjAKf8w' assert login.role == OperatorRole.operator assert login.legal_person_id == 'USWqY5cvkISJOxHyEKjAKf8w' - assert session.headers['X-Cuenca-SessionId'] == login.session_token + assert session.headers['X-Cuenca-SessionId'] == login.id assert 'X-Cuenca-LoginId' not in session.headers mock_request.assert_called_once()