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 [![Build 
Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:master+event:push)
 [![Coverage 
Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=master)
+# GrimoireLab Toolkit [![Build 
Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push)
 [![Coverage 
Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](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 [![Build 
Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:master+event:push)
 [![Coverage 
Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=master)
+# GrimoireLab Toolkit [![Build 
Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push)
 [![Coverage 
Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](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='')

Reply via email to