Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-grimoirelab-toolkit for
openSUSE:Factory checked in at 2026-03-16 14:16:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-grimoirelab-toolkit (Old)
and /work/SRC/openSUSE:Factory/.python-grimoirelab-toolkit.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-grimoirelab-toolkit"
Mon Mar 16 14:16:45 2026 rev:8 rq:1339139 version:1.2.5
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-grimoirelab-toolkit/python-grimoirelab-toolkit.changes
2024-09-09 14:45:19.148882810 +0200
+++
/work/SRC/openSUSE:Factory/.python-grimoirelab-toolkit.new.8177/python-grimoirelab-toolkit.changes
2026-03-16 14:19:53.055649655 +0100
@@ -1,0 +2,18 @@
+Sun Mar 15 19:00:16 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.2.5:
+ * Update Poetry's package dependencies
+ * Identity management functions\
+ * Introduce a new module for managing identities. It includes
+ functions to generate a UUID based on identity data and to
+ convert Unicode strings to their unaccented form.
+ * Increased minimum version for Python to 3.10\
+ * Python 3.9 reaches the end of life in October 2025. This
+ means it won't receive new updates or patches to fix security
+ issues.
+ * GrimoireLab supports Python 3.10 and higher from now on.
+ * Python 3.8 will reach its end of life in October 2024. Python
+ 3.9 is
+ * the minimum version required by the project.
+
+-------------------------------------------------------------------
Old:
----
grimoirelab_toolkit-1.0.4.tar.gz
New:
----
grimoirelab_toolkit-1.2.5.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-grimoirelab-toolkit.spec ++++++
--- /var/tmp/diff_new_pack.6ApoKi/_old 2026-03-16 14:19:53.459666427 +0100
+++ /var/tmp/diff_new_pack.6ApoKi/_new 2026-03-16 14:19:53.463666593 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-grimoirelab-toolkit
#
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,13 +18,13 @@
%{?sle15_python_module_pythons}
Name: python-grimoirelab-toolkit
-Version: 1.0.4
+Version: 1.2.5
Release: 0
Summary: Toolkit of common functions used across GrimoireLab
License: GPL-3.0-or-later
URL: https://chaoss.github.io/grimoirelab/
Source:
https://files.pythonhosted.org/packages/source/g/grimoirelab-toolkit/grimoirelab_toolkit-%{version}.tar.gz
-BuildRequires: %{python_module base >= 3.8}
+BuildRequires: %{python_module base >= 3.10}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module poetry-core >= 1.0.0}
BuildRequires: %{python_module pytest}
++++++ grimoirelab_toolkit-1.0.4.tar.gz -> grimoirelab_toolkit-1.2.5.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/grimoirelab_toolkit-1.0.4/NEWS
new/grimoirelab_toolkit-1.2.5/NEWS
--- old/grimoirelab_toolkit-1.0.4/NEWS 2024-08-09 11:29:26.100467200 +0200
+++ new/grimoirelab_toolkit-1.2.5/NEWS 1970-01-01 01:00:00.000000000 +0100
@@ -1,5 +1,98 @@
# Releases
+## grimoirelab-toolkit 1.2.5 - (2026-01-21)
+
+No changes list available.
+
+
+## grimoirelab-toolkit 1.2.4 - (2025-12-12)
+
+No changes list available.
+
+
+## grimoirelab-toolkit 1.2.3 - (2025-11-25)
+
+No changes list available.
+
+
+ ## grimoirelab-toolkit 1.2.2 - (2025-11-11)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.2.1 - (2025-10-31)
+
+ * Update Poetry's package dependencies
+
+## grimoirelab-toolkit 1.2.0 - (2025-10-10)
+
+**New features:**
+
+ * Identity management functions\
+ Introduce a new module for managing identities. It includes functions
+ to generate a UUID based on identity data and to convert Unicode
+ strings to their unaccented form.
+
+
+## grimoirelab-toolkit 1.1.0 - (2025-09-23)
+
+**New features:**
+
+ * Increased minimum version for Python to 3.10\
+ Python 3.9 reaches the end of life in October 2025. This means it
+ won't receive new updates or patches to fix security issues.
+ GrimoireLab supports Python 3.10 and higher from now on.
+
+
+ ## grimoirelab-toolkit 1.0.14 - (2025-08-18)
+
+ * Update Poetry's package dependencies
+
+## grimoirelab-toolkit 1.0.13 - (2025-06-19)
+
+**Bug fixes:**
+
+ * Deprecated utcfromtimestamp updated\
+ Class method `utcfromtimestamp` was deprecated in Python 3.12 and it
+ recommends using `fromtimestamp` with UTC instead.
+
+
+ ## grimoirelab-toolkit 1.0.12 - (2025-06-18)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.0.11 - (2025-06-03)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.0.10 - (2025-04-09)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.0.9 - (2025-01-15)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.0.8 - (2024-12-11)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.0.7 - (2024-11-13)
+
+ * Update Poetry's package dependencies
+
+ ## grimoirelab-toolkit 1.0.6 - (2024-10-15)
+
+ * Update Poetry's package dependencies
+
+## grimoirelab-toolkit 1.0.5 - (2024-09-23)
+
+**Dependencies updateds:**
+
+ * Python minimum version updated\
+ Python 3.8 will reach its end of life in October 2024. Python 3.9 is
+ the minimum version required by the project.
+
+
## grimoirelab-toolkit 1.0.4 - (2024-08-09)
* Update Poetry's package dependencies
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/grimoirelab_toolkit-1.0.4/PKG-INFO
new/grimoirelab_toolkit-1.2.5/PKG-INFO
--- old/grimoirelab_toolkit-1.0.4/PKG-INFO 1970-01-01 01:00:00.000000000
+0100
+++ new/grimoirelab_toolkit-1.2.5/PKG-INFO 1970-01-01 01:00:00.000000000
+0100
@@ -1,26 +1,29 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: grimoirelab-toolkit
-Version: 1.0.4
+Version: 1.2.5
Summary: Toolkit of common functions used across GrimoireLab
-Home-page: https://chaoss.github.io/grimoirelab/
License: GPL-3.0+
+License-File: AUTHORS
+License-File: LICENSE
Keywords: development,grimoirelab
Author: GrimoireLab Developers
-Requires-Python: >=3.8,<4.0
+Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU General Public License v3 or later
(GPLv3+)
Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
+Project-URL: Homepage, https://chaoss.github.io/grimoirelab/
Project-URL: Repository, https://github.com/chaoss/grimoirelab-toolkit
Description-Content-Type: text/markdown
-# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:master+event:push)
[](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=master)
+# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push)
[](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=main)
Toolkit of common functions used across GrimoireLab projects.
@@ -81,6 +84,121 @@
$ poetry shell
```
+## Credential Manager Library
+
+This is a module made to retrieve credentials from different secrets
management systems like Bitwarden.
+It accesses the secrets management service, looks for the desired credential
and returns it in String form.
+
+To use the module in your python code
+
+### Bitwarden
+
+```
+from grimoirelab_toolkit.credential_manager import BitwardenManager
+
+
+# Instantiate the Bitwarden manager using the api credentials for login
+bw_manager = BitwardenManager("your_client_id", "your_client_secret",
"your_master_password")
+
+# Login
+bw_manager.login()
+
+# Retrieve a secret from Bitwarden
+username = bw_manager.get_secret("github")
+password = bw_manager.get_secret("elasticsearch")
+
+# Logout
+bw_manager.logout()
+```
+
+
+#### Response format
+
+When calling `get_secret(item_name)`, the method returns a JSON object with
the following structure:
+
+_NOTE: the parameter "item_name" corresponds with the field "name" of the
json. That's the name of the item._
+(in this case, GitHub)
+
+
+##### Example Response
+
+ ```json
+ {
+ "passwordHistory": [
+ {
+ "lastUsedDate": "2024-11-05T10:27:18.411Z",
+ "password": "previous_password_value_1"
+ },
+ {
+ "lastUsedDate": "2024-11-05T09:20:06.512Z",
+ "password": "previous_password_value_2"
+ }
+ ],
+ "revisionDate": "2025-05-11T14:40:19.456Z",
+ "creationDate": "2024-10-30T18:56:41.023Z",
+ "object": "item",
+ "id": "91300380-620f-4707-8de1-b21901383315",
+ "organizationId": null,
+ "folderId": null,
+ "type": 1,
+ "reprompt": 0,
+ "name": "GitHub",
+ "notes": null,
+ "favorite": false,
+ "fields": [
+ {
+ "name": "api-token",
+ "value": "TOKEN"
+ "type": 0,
+ "linkedId": null
+ },
+ {
+ "name": "api_key",
+ "value": "APIKEY",
+ "type": 0,
+ "linkedId": null
+ }
+ ],
+ "login": {
+ "uris": [],
+ "username": "your_username",
+ "password": "your_password",
+ "totp": null,
+ "passwordRevisionDate": "2024-11-05T10:27:18.411Z"
+ },
+ "collectionIds": [],
+ "attachments": []
+ }
+```
+
+ Field Descriptions
+
+ - passwordHistory: Array of previously used passwords with timestamps
+ - revisionDate: Last modification timestamp (ISO 8601 format)
+ - creationDate: Item creation timestamp (ISO 8601 format)
+ - object: Always "item" for credential items
+ - id: Unique identifier for this item
+ - organizationId: Organization ID if shared, null for personal items
+ - folderId: Folder ID if organized, null otherwise
+ - type: Item type (1 = login, 2 = secure note, 3 = card, 4 = identity)
+ - name: Display name of the credential item (name used as argument in
get_secret())
+ - notes: Optional notes field
+ - favorite: Boolean indicating if item is favorited
+ - fields: Array of custom fields with name-value pairs
+ - name: Field name
+ - value: Field value (can contain secrets)
+ - type: Field type (0 = text, 1 = hidden, 2 = boolean)
+ - login: Login credentials object
+ - username: Login username
+ - password: Login password
+ - totp: TOTP secret for 2FA (if configured)
+ - uris: Array of associated URIs/URLs
+ - passwordRevisionDate: Last password change timestamp
+ - collectionIds: Array of collection IDs this item belongs to
+ - attachments: Array of file attachments
+
+The module uses the [Bitwarden CLI](https://bitwarden.com/help/cli/) to
interact with Bitwarden.
+
## License
Licensed under GNU General Public License (GPL), version 3 or later.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/grimoirelab_toolkit-1.0.4/README.md
new/grimoirelab_toolkit-1.2.5/README.md
--- old/grimoirelab_toolkit-1.0.4/README.md 2024-08-09 11:29:26.100467200
+0200
+++ new/grimoirelab_toolkit-1.2.5/README.md 1970-01-01 01:00:00.000000000
+0100
@@ -1,4 +1,4 @@
-# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:master+event:push)
[](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=master)
+# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push)
[](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=main)
Toolkit of common functions used across GrimoireLab projects.
@@ -59,6 +59,121 @@
$ poetry shell
```
+## Credential Manager Library
+
+This is a module made to retrieve credentials from different secrets
management systems like Bitwarden.
+It accesses the secrets management service, looks for the desired credential
and returns it in String form.
+
+To use the module in your python code
+
+### Bitwarden
+
+```
+from grimoirelab_toolkit.credential_manager import BitwardenManager
+
+
+# Instantiate the Bitwarden manager using the api credentials for login
+bw_manager = BitwardenManager("your_client_id", "your_client_secret",
"your_master_password")
+
+# Login
+bw_manager.login()
+
+# Retrieve a secret from Bitwarden
+username = bw_manager.get_secret("github")
+password = bw_manager.get_secret("elasticsearch")
+
+# Logout
+bw_manager.logout()
+```
+
+
+#### Response format
+
+When calling `get_secret(item_name)`, the method returns a JSON object with
the following structure:
+
+_NOTE: the parameter "item_name" corresponds with the field "name" of the
json. That's the name of the item._
+(in this case, GitHub)
+
+
+##### Example Response
+
+ ```json
+ {
+ "passwordHistory": [
+ {
+ "lastUsedDate": "2024-11-05T10:27:18.411Z",
+ "password": "previous_password_value_1"
+ },
+ {
+ "lastUsedDate": "2024-11-05T09:20:06.512Z",
+ "password": "previous_password_value_2"
+ }
+ ],
+ "revisionDate": "2025-05-11T14:40:19.456Z",
+ "creationDate": "2024-10-30T18:56:41.023Z",
+ "object": "item",
+ "id": "91300380-620f-4707-8de1-b21901383315",
+ "organizationId": null,
+ "folderId": null,
+ "type": 1,
+ "reprompt": 0,
+ "name": "GitHub",
+ "notes": null,
+ "favorite": false,
+ "fields": [
+ {
+ "name": "api-token",
+ "value": "TOKEN"
+ "type": 0,
+ "linkedId": null
+ },
+ {
+ "name": "api_key",
+ "value": "APIKEY",
+ "type": 0,
+ "linkedId": null
+ }
+ ],
+ "login": {
+ "uris": [],
+ "username": "your_username",
+ "password": "your_password",
+ "totp": null,
+ "passwordRevisionDate": "2024-11-05T10:27:18.411Z"
+ },
+ "collectionIds": [],
+ "attachments": []
+ }
+```
+
+ Field Descriptions
+
+ - passwordHistory: Array of previously used passwords with timestamps
+ - revisionDate: Last modification timestamp (ISO 8601 format)
+ - creationDate: Item creation timestamp (ISO 8601 format)
+ - object: Always "item" for credential items
+ - id: Unique identifier for this item
+ - organizationId: Organization ID if shared, null for personal items
+ - folderId: Folder ID if organized, null otherwise
+ - type: Item type (1 = login, 2 = secure note, 3 = card, 4 = identity)
+ - name: Display name of the credential item (name used as argument in
get_secret())
+ - notes: Optional notes field
+ - favorite: Boolean indicating if item is favorited
+ - fields: Array of custom fields with name-value pairs
+ - name: Field name
+ - value: Field value (can contain secrets)
+ - type: Field type (0 = text, 1 = hidden, 2 = boolean)
+ - login: Login credentials object
+ - username: Login username
+ - password: Login password
+ - totp: TOTP secret for 2FA (if configured)
+ - uris: Array of associated URIs/URLs
+ - passwordRevisionDate: Last password change timestamp
+ - collectionIds: Array of collection IDs this item belongs to
+ - attachments: Array of file attachments
+
+The module uses the [Bitwarden CLI](https://bitwarden.com/help/cli/) to
interact with Bitwarden.
+
## License
Licensed under GNU General Public License (GPL), version 3 or later.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/_version.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/_version.py
--- old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/_version.py
2024-08-09 11:29:26.100467200 +0200
+++ new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/_version.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,2 +1,2 @@
-# File auto-generated by semverup on 2024-08-09 09:29:13.850712
-__version__ = "1.0.4"
+# File auto-generated by semverup on 2026-01-21 10:14:11.603202
+__version__ = "1.2.5"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/__init__.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/__init__.py
---
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Grimoirelab Contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author:
+# Alberto Ferrer Sánchez ([email protected])
+#
+
+from .bw_manager import BitwardenManager
+from .exceptions import (
+ CredentialManagerError,
+ InvalidCredentialsError,
+ CredentialNotFoundError,
+ BitwardenCLIError,
+)
+
+__all__ = [
+ "BitwardenManager",
+ "CredentialManagerError",
+ "InvalidCredentialsError",
+ "CredentialNotFoundError",
+ "BitwardenCLIError",
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/__main__.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/__main__.py
---
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/__main__.py
1970-01-01 01:00:00.000000000 +0100
+++
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/__main__.py
1970-01-01 01:00:00.000000000 +0100
@@ -0,0 +1,4 @@
+from .credential_manager import main
+
+if __name__ == "__main__":
+ main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/bw_manager.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/bw_manager.py
---
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/bw_manager.py
1970-01-01 01:00:00.000000000 +0100
+++
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/bw_manager.py
1970-01-01 01:00:00.000000000 +0100
@@ -0,0 +1,212 @@
+#
+#
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author:
+# Alberto Ferrer Sánchez ([email protected])
+#
+import json
+import subprocess
+import logging
+import shutil
+
+from .exceptions import (
+ BitwardenCLIError,
+ InvalidCredentialsError,
+ CredentialNotFoundError,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class BitwardenManager:
+ """Retrieve credentials from Bitwarden.
+
+ This class defines functions to log in, retrieve secrets
+ and log out of Bitwarden using the Bitwarden CLI. The
+ workflow is:
+
+ manager = BitwardenManager(client_id, client_secret, master_password)
+ manager.login()
+ manager.get_secret("github")
+ manager.get_secret("elasticsearch")
+ manager.logout()
+
+ The manager logs in using the client_id, client_secret, and
+ master_password given as arguments when creating the instance,
+ so the object is reusable along the program.
+
+ The path of Bitwarden CLI (bw) is retrieved using shutil.
+ """
+
+ def __init__(self, client_id: str, client_secret: str, master_password:
str):
+ """
+ Creates BitwardenManager object using API key authentication
+
+ :param str client_id: Bitwarden API client ID
+ :param str client_secret: Bitwarden API client secret
+ :param str master_password: Master password for unlocking the vault
+ """
+ # Session key of the bw session
+ self.session_key = None
+
+ # API credentials
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.master_password = master_password
+
+ # Get the absolute path to the bw executable
+ self.bw_path = shutil.which("bw")
+ if not self.bw_path:
+ raise BitwardenCLIError("Bitwarden CLI (bw) not found in PATH")
+
+ # Set up environment variables for consistent execution context
+ self.env = {
+ "LANG": "C",
+ "BW_CLIENTID": client_id,
+ "BW_CLIENTSECRET": client_secret,
+ }
+
+ def login(self) -> str | None:
+ """Log into Bitwarden.
+
+ Use the API authentication key to log in and unlock the vault. After
it,
+ it will obtain a session key that will be used by to access the vault.
+
+ :returns: The session key for the current Bitwarden session.
+
+ :raises InvalidCredentialsError: If invalid credentials are provided
+ :raises BitwardenCLIError: If Bitwarden CLI operations fail
+ """
+ # Log in using API key
+ login_result = subprocess.run(
+ [self.bw_path, "login", "--apikey"],
+ input=f"{self.client_id}\n{self.client_secret}\n",
+ capture_output=True,
+ text=True,
+ env=self.env,
+ )
+
+ if login_result.returncode != 0:
+ error_msg = (
+ login_result.stderr.strip() if login_result.stderr else
"Unknown error"
+ )
+ logger.error("Error logging in with API key: %s", error_msg)
+ raise InvalidCredentialsError(
+ "Invalid API credentials provided for Bitwarden"
+ )
+
+ # After login, we need to unlock the vault to get a session key
+ self.session_key = self._unlock_vault()
+
+ return self.session_key
+
+ def _unlock_vault(self) -> str:
+ """Unlock the vault after authentication.
+
+ Executes the Bitwarden unlock command to obtain a session key
+ for an already authenticated user but locked vault.
+
+ :returns: Session key for the unlocked vault
+ :raises BitwardenCLIError: If unlock operation fails or returns empty
session key
+ """
+ # this uses the master password to unlock the vault
+ unlock_result = subprocess.run(
+ [self.bw_path, "unlock", "--raw"],
+ input=f"{self.master_password}\n",
+ capture_output=True,
+ text=True,
+ env=self.env,
+ )
+
+ if unlock_result.returncode != 0:
+ error_msg = (
+ unlock_result.stderr.strip()
+ if unlock_result.stderr
+ else "Unknown error"
+ )
+ logger.error("Error unlocking vault: %s", error_msg)
+ raise BitwardenCLIError(f"Failed to unlock vault: {error_msg}")
+
+ # the session key is used when retrieving the secrets with get_secret
+ session_key = unlock_result.stdout.strip()
+ if not session_key:
+ raise BitwardenCLIError("Empty session key received from unlock
command")
+
+ return session_key
+
+ def get_secret(self, item_name: str) -> dict:
+ """Retrieve an item from the Bitwarden vault.
+
+ Retrieves all the fields stored for an item with the name
+ provided as an argument and returns them as a dictionary.
+
+ The returned dictionary includes fields such as:
+ - login: username, password, URIs, TOTP
+ - fields: custom fields
+ - notes: secure notes
+ - name, id, and other metadata
+
+ :param str item_name: The name of the item to retrieve
+
+ :returns: Dictionary containing the item data
+ :rtype: dict
+
+ :raises CredentialNotFoundError: If the specific credential is not
found
+ :raises BitwardenCLIError: If Bitwarden CLI operations fail
+ """
+ # Pass session key via command line parameter
+ result = subprocess.run(
+ [self.bw_path, "get", "item", item_name, "--session",
self.session_key],
+ capture_output=True,
+ text=True,
+ env=self.env,
+ )
+
+ if result.returncode != 0:
+ raise CredentialNotFoundError(f"Credential not found:
'{item_name}'")
+
+ # Parse the JSON response returned in stdout
+ try:
+ item = json.loads(result.stdout)
+ except json.JSONDecodeError as e:
+ logger.error("Failed to parse Bitwarden response: %s", str(e))
+ raise BitwardenCLIError(f"Invalid JSON response from Bitwarden:
{e}")
+
+ return item
+
+ def logout(self) -> None:
+ """Log out from Bitwarden and invalidate the session.
+
+ This method ends the current session and clears the session key.
+ """
+ logger.info("Logging out from Bitwarden")
+
+ # Execute logout command
+ result = subprocess.run(
+ [self.bw_path, "logout"],
+ capture_output=True,
+ text=True,
+ env=self.env,
+ )
+
+ if result.returncode != 0:
+ error_msg = result.stderr.strip() if result.stderr else "Unknown
error"
+ logger.error("Error during logout: %s", error_msg)
+
+ # Clear session key for security
+ self.session_key = None
+
+ logger.info("Successfully logged out from Bitwarden")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/exceptions.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/exceptions.py
---
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/credential_manager/exceptions.py
1970-01-01 01:00:00.000000000 +0100
+++
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/credential_manager/exceptions.py
1970-01-01 01:00:00.000000000 +0100
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Grimoirelab Contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author:
+# Alberto Ferrer Sánchez ([email protected])
+#
+
+"""Custom exceptions for the credential manager module."""
+
+__all__ = [
+ "CredentialManagerError",
+ "InvalidCredentialsError",
+ "CredentialNotFoundError",
+ "BitwardenCLIError",
+]
+
+
+class CredentialManagerError(Exception):
+ """Base exception for all credential manager errors."""
+
+ pass
+
+
+class InvalidCredentialsError(CredentialManagerError):
+ """Raised when invalid credentials are provided."""
+
+ pass
+
+
+class CredentialNotFoundError(CredentialManagerError):
+ """Raised when a specific credential is not found in a secret."""
+
+ pass
+
+
+class BitwardenCLIError(CredentialManagerError):
+ """Raised for Bitwarden CLI specific errors."""
+
+ pass
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/datetime.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/datetime.py
--- old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/datetime.py
2024-08-09 11:29:26.100467200 +0200
+++ new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/datetime.py
1970-01-01 01:00:00.000000000 +0100
@@ -175,7 +175,7 @@
converted into a valid date
"""
try:
- dt = datetime.datetime.utcfromtimestamp(ut)
+ dt = datetime.datetime.fromtimestamp(ut, datetime.timezone.utc)
dt = dt.replace(tzinfo=dateutil.tz.tzutc())
return dt
except Exception:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/identities.py
new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/identities.py
--- old/grimoirelab_toolkit-1.0.4/grimoirelab_toolkit/identities.py
1970-01-01 01:00:00.000000000 +0100
+++ new/grimoirelab_toolkit-1.2.5/grimoirelab_toolkit/identities.py
1970-01-01 01:00:00.000000000 +0100
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Grimoirelab developers
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Code copied from the SortingHat repository:
+# https://github.com/chaoss/grimoirelab-sortinghat/
+# Originally copyrighted by Bitergia
+#
+
+import hashlib
+import unicodedata
+
+
+def unaccent_string(unistr):
+ """Convert a Unicode string to its canonical form without accents.
+
+ This allows to convert Unicode strings which include accent
+ characters to their unaccent canonical form. For instance,
+ characters 'Ê, ê, é, ë' are considered the same character as 'e';
+ characters 'Ĉ, ć' are the same as 'c'.
+
+ :param unistr: Unicode string to unaccent
+
+ :returns: Unicode string on its canonical form
+ """
+ if not isinstance(unistr, str):
+ msg = "argument must be a string; {}
given".format(unistr.__class__.__name__)
+ raise TypeError(msg)
+
+ cs = [c for c in unicodedata.normalize('NFD', unistr)
+ if unicodedata.category(c) != 'Mn']
+ string = ''.join(cs)
+
+ return string
+
+
+def generate_uuid(source, email=None, name=None, username=None):
+ """Generate a UUID related to identity data.
+
+ Based on the input data, the function will return the UUID
+ associated to an identity. On this version, the UUID will
+ be the SHA1 of `source:email:name:username` string.
+
+ This string is case insensitive, which means same values
+ for the input parameters in upper or lower case will produce
+ the same UUID.
+
+ The value of `name` will converted to its unaccent form which
+ means same values with accent or unaccent chars (i.e 'ö and o')
+ will generate the same UUID.
+
+ For instance, these combinations will produce the same UUID:
+ ('scm', '[email protected]', 'John Smith', 'jsmith'),
+ ('scm', 'jsmith@example,com', 'Jöhn Smith', 'jsmith'),
+ ('scm', '[email protected]', 'John Smith', 'JSMITH'),
+ ('scm', '[email protected]', 'john Smith', 'jsmith')
+
+ :param source: data source
+ :param email: email of the identity
+ :param name: full name of the identity
+ :param username: user name used by the identity
+
+ :returns: a universal unique identifier for Sorting Hat
+
+ :raises ValueError: when source is `None` or empty; each one
+ of the parameters is `None`; or the parameters are empty.
+ """
+ def to_str(value, unaccent=False):
+ s = str(value)
+ if unaccent:
+ return unaccent_string(s)
+ else:
+ return s
+
+ if source is None:
+ raise ValueError("'source' cannot be None")
+ if source == '':
+ raise ValueError("'source' cannot be an empty string")
+ if not (email or name or username):
+ raise ValueError("identity data cannot be empty")
+
+ s = ':'.join((to_str(source),
+ to_str(email),
+ to_str(name, unaccent=True),
+ to_str(username))).lower()
+ s = s.encode('UTF-8', errors="surrogateescape")
+
+ sha1 = hashlib.sha1(s)
+ uuid_ = sha1.hexdigest()
+
+ return uuid_
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/grimoirelab_toolkit-1.0.4/pyproject.toml
new/grimoirelab_toolkit-1.2.5/pyproject.toml
--- old/grimoirelab_toolkit-1.0.4/pyproject.toml 2024-08-09
11:29:26.100467200 +0200
+++ new/grimoirelab_toolkit-1.2.5/pyproject.toml 1970-01-01
01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
[tool.poetry]
name = "grimoirelab-toolkit"
-version = "1.0.4"
+version = "1.2.5"
description = "Toolkit of common functions used across GrimoireLab"
authors = [
"GrimoireLab Developers"
@@ -37,11 +37,11 @@
]
[tool.poetry.dependencies]
-python = "^3.8"
+python = "^3.10"
python-dateutil = "^2.8.2"
-[tool.poetry.dev-dependencies]
-flake8 = "^4.0.1"
+[tool.poetry.group.dev.dependencies]
+flake8 = "^7.1.1"
coverage = "^7.2.3"
[build-system]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/grimoirelab_toolkit-1.0.4/tests/test_bw_manager.py
new/grimoirelab_toolkit-1.2.5/tests/test_bw_manager.py
--- old/grimoirelab_toolkit-1.0.4/tests/test_bw_manager.py 1970-01-01
01:00:00.000000000 +0100
+++ new/grimoirelab_toolkit-1.2.5/tests/test_bw_manager.py 1970-01-01
01:00:00.000000000 +0100
@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Grimoirelab Contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Author:
+# Alberto Ferrer Sánchez ([email protected])
+#
+
+import unittest
+from unittest.mock import patch, MagicMock, call
+
+from grimoirelab_toolkit.credential_manager.bw_manager import BitwardenManager
+from grimoirelab_toolkit.credential_manager.exceptions import (
+ InvalidCredentialsError,
+ BitwardenCLIError,
+ CredentialNotFoundError,
+)
+
+
+class TestBitwardenManager(unittest.TestCase):
+ """Tests for BitwardenManager class."""
+
+ def setUp(self):
+ """Set up common test fixtures."""
+
+ self.client_id = "test_client_id"
+ self.client_secret = "test_client_secret"
+ self.master_password = "test_master_password"
+ self.session_key = "test_session_key"
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_initialization_success(self, mock_which):
+ """Test successful initialization with valid credentials."""
+
+ mock_which.return_value = "/usr/bin/bw"
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+
+ self.assertEqual(manager.client_id, self.client_id)
+ self.assertEqual(manager.client_secret, self.client_secret)
+ self.assertEqual(manager.master_password, self.master_password)
+ self.assertIsNone(manager.session_key)
+ self.assertEqual(manager.bw_path, "/usr/bin/bw")
+ self.assertIn("BW_CLIENTID", manager.env)
+ self.assertIn("BW_CLIENTSECRET", manager.env)
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_initialization_bw_not_found(self, mock_which):
+ """Test initialization fails when bw CLI is not found."""
+
+ mock_which.return_value = None
+
+ with self.assertRaises(BitwardenCLIError) as context:
+ BitwardenManager(self.client_id, self.client_secret,
self.master_password)
+
+ self.assertIn("Bitwarden CLI (bw) not found in PATH",
str(context.exception))
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_login_success(self, mock_which, mock_run):
+ """Test successful login and unlock."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ mock_run.side_effect = [
+ MagicMock(returncode=0, stdout="Logged in!", stderr=""), # login
+ MagicMock(returncode=0, stdout="test_session_key\n", stderr=""),
# unlock
+ ]
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ session_key = manager.login()
+
+ self.assertEqual(session_key, "test_session_key")
+ self.assertEqual(manager.session_key, "test_session_key")
+ self.assertEqual(mock_run.call_count, 2)
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_login_failure(self, mock_which, mock_run):
+ """Test login failure with invalid credentials."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ mock_run.return_value = MagicMock(returncode=1, stderr="Invalid
credentials")
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+
+ with self.assertRaises(InvalidCredentialsError):
+ manager.login()
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_unlock_failure(self, mock_which, mock_run):
+ """Test unlock failure after successful login."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ mock_run.side_effect = [
+ MagicMock(returncode=0, stdout="Logged in!", stderr=""), # login
+ MagicMock(returncode=1, stderr="Unlock failed", stdout=""), #
unlock
+ ]
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+
+ with self.assertRaises(BitwardenCLIError) as context:
+ manager.login()
+
+ self.assertIn("Failed to unlock vault", str(context.exception))
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_get_secret_success(self, mock_which, mock_run):
+ """Test successful secret retrieval."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ secret_result = MagicMock(
+ returncode=0,
stdout='{"name":"github","login":{"password":"secret123"}}'
+ )
+ mock_run.return_value = secret_result
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ manager.session_key = self.session_key
+ result = manager.get_secret("github")
+
+ # Now returns a parsed dict, not subprocess result
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result["name"], "github")
+ self.assertEqual(result["login"]["password"], "secret123")
+ mock_run.assert_called_once()
+
+ # Verify the get_secret call includes session key
+ call_args = mock_run.call_args
+ self.assertEqual(
+ call_args[0][0],
+ ["/usr/bin/bw", "get", "item", "github", "--session",
self.session_key],
+ )
+ self.assertTrue(call_args[1]["capture_output"])
+ self.assertTrue(call_args[1]["text"])
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_get_secret_returns_parsed_dict(self, mock_which, mock_run):
+ """Test that get_secret returns parsed dict from JSON response."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ secret_result = MagicMock(returncode=0, stdout='{"data":"value"}',
stderr="")
+ mock_run.return_value = secret_result
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ manager.session_key = self.session_key
+ result = manager.get_secret("my_item")
+
+ # The method returns a parsed dict, not subprocess result
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result["data"], "value")
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_get_secret_not_found(self, mock_which, mock_run):
+ """Test get_secret raises error when item not found."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ secret_result = MagicMock(returncode=1, stderr="Not found")
+ mock_run.return_value = secret_result
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ manager.session_key = self.session_key
+
+ with self.assertRaises(CredentialNotFoundError) as context:
+ manager.get_secret("nonexistent")
+
+ self.assertIn("Credential not found", str(context.exception))
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_get_secret_invalid_json(self, mock_which, mock_run):
+ """Test get_secret raises error when response is not valid JSON."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ secret_result = MagicMock(returncode=0, stdout="not valid json")
+ mock_run.return_value = secret_result
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ manager.session_key = self.session_key
+
+ with self.assertRaises(BitwardenCLIError) as context:
+ manager.get_secret("github")
+
+ self.assertIn("Invalid JSON response", str(context.exception))
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_logout_success(self, mock_which, mock_run):
+ """Test successful logout clears session data."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ mock_run.side_effect = [
+ MagicMock(returncode=0, stdout="Logged in!"), # login
+ MagicMock(returncode=0, stdout="test_session_key"), # unlock
+ MagicMock(returncode=0, stdout="You have logged out."), # logout
+ ]
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ manager.login()
+
+ self.assertEqual(manager.session_key, "test_session_key")
+
+ manager.logout()
+
+ self.assertIsNone(manager.session_key)
+ self.assertEqual(mock_run.call_count, 3)
+
+ # Verify logout was called
+ logout_call = call(
+ ["/usr/bin/bw", "logout"], capture_output=True, text=True,
env=manager.env
+ )
+ self.assertIn(logout_call, mock_run.call_args_list)
+
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
+ @patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
+ def test_logout_failure_still_clears_data(self, mock_which, mock_run):
+ """Test logout still clears session data even when command fails."""
+
+ mock_which.return_value = "/usr/bin/bw"
+ mock_run.side_effect = [
+ MagicMock(returncode=0, stdout="Logged in!"), # login
+ MagicMock(returncode=0, stdout="test_session_key"), # unlock
+ MagicMock(returncode=1, stderr="Logout failed"), # logout
+ ]
+
+ manager = BitwardenManager(
+ self.client_id, self.client_secret, self.master_password
+ )
+ manager.login()
+ manager.logout()
+
+ self.assertIsNone(manager.session_key)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/grimoirelab_toolkit-1.0.4/tests/test_identities.py
new/grimoirelab_toolkit-1.2.5/tests/test_identities.py
--- old/grimoirelab_toolkit-1.0.4/tests/test_identities.py 1970-01-01
01:00:00.000000000 +0100
+++ new/grimoirelab_toolkit-1.2.5/tests/test_identities.py 1970-01-01
01:00:00.000000000 +0100
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Grimoirelab developers
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Code copied from the SortingHat repository:
+# https://github.com/chaoss/grimoirelab-sortinghat/
+# Originally copyrighted by Bitergia
+#
+
+import unittest
+
+from grimoirelab_toolkit.identities import unaccent_string, generate_uuid
+
+UNACCENT_TYPE_ERROR = "argument must be a string; int given"
+IDENTITY_NONE_OR_EMPTY_ERROR = "identity data cannot be empty"
+SOURCE_NONE_OR_EMPTY_ERROR = "'source' cannot be"
+
+
+class TestUnnacentString(unittest.TestCase):
+ """Unit tests for unaccent_string"""
+
+ def test_unaccent(self):
+ """Check unicode casting removing accents"""
+
+ result = unaccent_string('Tomáš Čechvala')
+ self.assertEqual(result, 'Tomas Cechvala')
+
+ result = unaccent_string('Tomáš Čechvala')
+ self.assertEqual(result, 'Tomas Cechvala')
+
+ result = unaccent_string('Santiago Dueñas')
+ self.assertEqual(result, 'Santiago Duenas')
+
+ def test_no_string(self):
+ """Check if an exception is raised when the type is not a string"""
+
+ with self.assertRaisesRegex(TypeError, UNACCENT_TYPE_ERROR):
+ unaccent_string(1234)
+
+
+class TestUUID(unittest.TestCase):
+ """Unit tests for generate_uuid function"""
+
+ def test_uuid(self):
+ """Check whether the function returns the expected UUID"""
+
+ result = generate_uuid('scm', email='[email protected]',
+ name='John Smith', username='jsmith')
+ self.assertEqual(result, 'a9b403e150dd4af8953a52a4bb841051e4b705d9')
+
+ result = generate_uuid('scm', email='[email protected]')
+ self.assertEqual(result, '334da68fcd3da4e799791f73dfada2afb22648c6')
+
+ result = generate_uuid('scm', email='', name='John Smith',
username='jsmith')
+ self.assertEqual(result, 'a4b4591c3a2171710c157d7c278ea3cc03becf81')
+
+ result = generate_uuid('scm', email='', name='John Smith', username='')
+ self.assertEqual(result, '76e3624e24aacae178d05352ad9a871dfaf81c13')
+
+ result = generate_uuid('scm', email='', name='', username='jsmith')
+ self.assertEqual(result, '6e7ce2426673f8a23a72a343b1382dda84c0078b')
+
+ result = generate_uuid('scm', email='', name='John Ca\xf1as',
username='jcanas')
+ self.assertEqual(result, 'c88e126749ff006eb1eea25e4bb4c1c125185ed2')
+
+ result = generate_uuid('scm', email='', name="Max Müster",
username='mmuester')
+ self.assertEqual(result, '9a0498297d9f0b7e4baf3e6b3740d22d2257367c')
+
+ def test_case_insensitive(self):
+ """Check if same values in lower or upper case produce the same UUID"""
+
+ uuid_a = generate_uuid('scm', email='[email protected]',
+ name='John Smith', username='jsmith')
+ uuid_b = generate_uuid('SCM', email='[email protected]',
+ name='John Smith', username='jsmith')
+ self.assertEqual(uuid_a, uuid_b)
+
+ uuid_c = generate_uuid('scm', email='[email protected]',
+ name='john smith', username='jsmith')
+ self.assertEqual(uuid_c, uuid_a)
+
+ uuid_d = generate_uuid('scm', email='[email protected]',
+ name='John Smith', username='JSmith')
+ self.assertEqual(uuid_d, uuid_a)
+
+ uuid_e = generate_uuid('scm', email='[email protected]',
+ name='John Smith', username='jsmith')
+ self.assertEqual(uuid_e, uuid_a)
+
+ def test_case_unaccent_name(self):
+ """Check if same values accent or unaccent produce the same UUID"""
+
+ accent_result = generate_uuid('scm', email='', name="Max Müster",
username='mmuester')
+ unaccent_result = generate_uuid('scm', email='', name="Max Muster",
username='mmuester')
+ self.assertEqual(accent_result, unaccent_result)
+ self.assertEqual(accent_result,
'9a0498297d9f0b7e4baf3e6b3740d22d2257367c')
+
+ accent_result = generate_uuid('scm', email='', name="Santiago Dueñas",
username='')
+ unaccent_result = generate_uuid('scm', email='', name="Santiago
Duenas", username='')
+ self.assertEqual(accent_result, unaccent_result)
+ self.assertEqual(accent_result,
'0f1dd18839007ee8a11d02572ca0a0f4eedaf2cd')
+
+ accent_result = generate_uuid('scm', email='', name="Tomáš Čechvala",
username='')
+ partial_accent_result = generate_uuid('scm', email='', name="Tomáš
Cechvala", username='')
+ unaccent_result = generate_uuid('scm', email='', name="Tomas
Cechvala", username='')
+ self.assertEqual(accent_result, unaccent_result)
+ self.assertEqual(accent_result, partial_accent_result)
+
+ def test_surrogate_escape(self):
+ """Check if no errors are raised for invalid UTF-8 chars"""
+
+ result = generate_uuid('scm', name="Mishal\udcc5 Pytasz")
+ self.assertEqual(result, '625166bdc2c4f1a207d39eb8d25315010babd73b')
+
+ def test_none_source(self):
+ """Check whether UUID cannot be obtained giving a None source"""
+
+ with self.assertRaisesRegex(ValueError, SOURCE_NONE_OR_EMPTY_ERROR):
+ generate_uuid(None)
+
+ def test_empty_source(self):
+ """Check whether UUID cannot be obtained giving aadded to the
registry"""
+
+ with self.assertRaisesRegex(ValueError, SOURCE_NONE_OR_EMPTY_ERROR):
+ generate_uuid('')
+
+ def test_none_or_empty_data(self):
+ """Check whether UUID cannot be obtained when identity data is None or
empty"""
+
+ with self.assertRaisesRegex(ValueError, IDENTITY_NONE_OR_EMPTY_ERROR):
+ generate_uuid('scm', email=None, name='', username=None)
+
+ with self.assertRaisesRegex(ValueError, IDENTITY_NONE_OR_EMPTY_ERROR):
+ generate_uuid('scm', email='', name='', username='')