Repository: gnome-pomodoro/gnome-pomodoro-tracking (language: python)

Issue:
Adjust commands
You recommend to use `alias gp-tracking=gnome-pomodoro-tracking`, but the alias isn't permanent.

Therefore `gp-tracking -gps "$(state)" -gpt "$(triggers)" -gpd "$(duration)" -gpe "$(elapsed)"` doesn't work after a restart, only `gnome-pomodoro-tracking "$(state)" -gpt "$(triggers)" -gpd "$(duration)" -gpe "$(elapsed)"` does.

Can you either provide a command for a permanent alias or adjust the commands to `gnome-pomodoro-tracking` in the readme and the other instructions in order to prevent confusion?


Discussion / hints:
Thanks for the report. I've  started updating README

Fix patch:
diff --git a/.gitignore b/.gitignore
index db31029..5f3b056 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,4 +130,6 @@ dmypy.json
 tests/stdout.txt
 assets/gnome-pomodoro-256x256.png
 assets/how-does-work.xcf
-assets/gitlab-logo2.png
\ No newline at end of file
+assets/gitlab-logo2.png
+gnome-pomodoro-tracking.conf
+Pipfile
\ No newline at end of file
diff --git a/.pm/author.yml b/.pm/author.yml
new file mode 100644
index 0000000..e63478e
--- /dev/null
+++ b/.pm/author.yml
@@ -0,0 +1,3 @@
+authors:
+- Jose Hbez <https://github.com/josehbez>
+- GNOME Pomodoro <https://gnomepomodoro.org>
diff --git a/.pm/changelog.yml b/.pm/changelog.yml
new file mode 100644
index 0000000..8ae5315
--- /dev/null
+++ b/.pm/changelog.yml
@@ -0,0 +1,16 @@
+3.0.0 (2020-12-07):
+  added:
+  - workflow
+  - Test odoo plugin
+  - Test toggl plugin
+  - Test clockify plugin
+  - Track time entry,  if time(minutes) elapsed  is greater zero
+  - Clockify , toogl and odoo now inehrit by GPTPlugin
+  - Rename arg --state by --status
+  - Rename arg --kill by long arg --stop
+
+3.0.1 (2021-01-09):
+  added:
+  - Rename repo josehbez/gp-tracking to gnome-pomodoro/gnome-pomodoro-tracking
+  - Rename bin gp-tracking to gnome-pomodoro-tracking
+  - File gpt.conf replace field name  Description by type
\ No newline at end of file
diff --git a/.pm/pm.yml b/.pm/pm.yml
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/.pm/pm.yml
@@ -0,0 +1,1 @@
+{}
diff --git a/.pm/version.yml b/.pm/version.yml
new file mode 100644
index 0000000..5149fb0
--- /dev/null
+++ b/.pm/version.yml
@@ -0,0 +1,4 @@
+version:
+  major: 3
+  minor: 1
+  patch: 0
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index c94c43f..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# 3.0.1
-
-* Rename repo josehbez/gp-tracking to gnome-pomodoro/gnome-pomodoro-tracking
-* Rename bin gp-tracking to gnome-pomodoro-tracking
-* File gpt.conf replace field name  Description by type
-
-# 3.0.0
-
-* Clockify , toogl and odoo now inehrit by GPTPlugin
-* Rename arg --kill by long arg --stop
-* Rename arg --reset by --restart
-* Rename arg --state by --status
-* doc: parameters  documentation
-* fix: Track time entry,  if time(minutes) elapsed  is greater zero
-* add: Test clockify plugin
-* add: Test toggl plugin
-* add: Test odoo plugin
-* add: workflow tests
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 00da4f1..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Build a plugin
-
-Create a folder in `plugins`
-```bash
- mkdir -p ./plugins/clockify
-```
-
-Create a file `python` 
-
-```bash
-touch ./plugins/clockify/clockify.py
-```
-
-Create a `class` using the following template
-
-```python
-
-class Clockify:
-    
-    # Es usado para crear una seccion en el archivo ~./gp-tracking.conf
-    GTP_CONFIG = "clockify"
-    
-    def __init__(self, gptracking):
-        self.gptracking = gptracking
-
-    
-    def add_time_entry(self, **kwargs):
-        """ 
-        Method used to register a time entry with the provider
-        params:
-            description: Description activity
-            start: datetime in UTC format %Y-%m-%dT%H:%M:%SZ
-            end: datetime in UTC format %Y-%m-%dT%H:%M:%SZ
-            minutes: elapsed
-        return: True or False
-        """
-        raise NotImplementedError
- 
-    def gptparse_args(self, **kwargs):
-        """ 
-        Method to add optional arguments in command terminal
-        params: None
-        return: None
-        
-        e.g
-        
-        self.gptracking.parse.add_argument('--clockify-workspaces', 
-            action='store_const', 
-            dest='clockify_workspaces', 
-            help='List clockify workspaces',     
-            const=True,                
-        )
-        """
-        raise NotImplementedError
-
-    def cli(self):
-        """
-        Method that interprets command line arguments
-        params: None
-        return: None
-
-        e.g 
-
-        params = self.gptracking.gptparse_params()
-        if params.clockify_workspaces:
-            pass
-        """
-        raise NotImplementedError
-
-    def state(self, **kwargs):
-        """
-        Method to print the current state of the Pomodoro on the command line
-        params: None
-        return: None
-
-        e.g 
-
-        _name = self.gptracking.gptconfig_get(self.GTP_CONFIG, "clockify_name")
-        self.gptracking.print_cli([{'name': _name }])
-        """
-        raise NotImplementedError
-
-
-```
-
-
-Thanks for contributing
\ No newline at end of file
diff --git a/README.md b/README.md
index 0c67878..976fd54 100644
--- a/README.md
+++ b/README.md
@@ -9,33 +9,31 @@
   <a href="LICENSE">  
     <img src="https://img.shields.io/github/license/gnome-pomodoro/gnome-pomodoro-tracking?style=flat-square" />
   </a>
-   <a href="semv.toml">
-    <img src="https://img.shields.io/badge/semv-3.0.1-green">
+  <a href=".pm/version.yml">
+        <img src="https://img.shields.io/badge/dynamic/yaml?color=green&label=version&query=version.*&url=https://raw.githubusercontent.com/gnome-pomodoro/gnome-pomodoro-tracking/master/.pm/version.yml">
   </a>
-  
 
 </p>
 
-## Gnome Pomodoro Tracking (gp-tracking)
-It is a custom action for Gnome Pomodoro, that to connect any Time Tracking Software and create Time Entries.
+# GNOME Pomodoro Tracking
+Lets you track your time with the popular time tracking services.
 
 ## Contents
 
 * [Requirements](#requirements)
 * [Install](#install)
 * [CLI](#cli)
-* [Plugins](#plugins)
-  * [Clockify](#clockify)
-  * [Toggl](#toggl)
-  * [Odoo](#odoo)
-* [Gnome Pomodoro Settings](#gnome-pomodoro-settings)
-* [Contributing](#contributing)
+* Plugins
+  * [Toggl](./assets/toggl/README.md)
+  * [Clockify](./assets/clockify/README.md)
+  * [Odoo](./assets/odoo/README.md)
+* [GNOME Pomodoro Settings](#gnome-pomodoro-settings)
 * [Tests](#tests)
 
 ## Requirements
 
 * python3
-* git 
+* git
 * [gnomepomodoro.org](https://gnomepomodoro.org)
 
 ## Install
@@ -55,56 +53,51 @@ sh -c "$(curl -fsSL https://raw.githubusercontent.com/gnome-pomodoro/gnome-pomod
 ## CLI
 
 ```bash
-# set alias or any short-name
-alias gp-tracking=gnome-pomodoro-tracking
 
-# Select Time Tracking Software
-gp-tracking --plugin NAME  
+usage: gnome-pomodoro-tracking [-h] [--plugin {odoo,clockify,toggl}] [-n NAME]
+                               [-r] [-k] [-s]
 
-#Enter the name of the time entry. If there is no active Pomodoro start a new one.
-gp-tracking --name NAME
-
-# Displays the summary of the time entry 
-gp-tracking --status
-
-# Stop the active Pomodoro, starts a new time entry
-gp-tracking --restart
-
-# Stop the active Pomodoro
-gp-tracking --stop
+optional arguments:
+  -h, --help            show this help message and exit
+  --plugin {odoo,clockify,toggl}
+                        Select Time Tracking Service
+  -n NAME, --name NAME  Set the name of the time entry
+  -r, --restart         Restart Pomodoro
+  -k, --stop            Stop Pomodoro
+  -s, --status          Status Pomodoro
 
 ```
 
-## Plugins
-
-### Clockify
-
-[Guidelines](./assets/clockify/README.md)
-
-### Toggl
-
-[Guidelines](./assets/toggl/README.md)
-
-### Odoo
-
-[Guidelines](./assets/odoo/README.md)
-
-## Gnome Pomodoro Settings
+## GNOME Pomodoro Settings
 
 `Preferences / Plugins ... / Custom Actions(Execute shell scripts) / Add `
 
 ```bash
-gp-tracking -gps "$(state)" -gpt "$(triggers)" -gpd "$(duration)" -gpe "$(elapsed)"
+gnome-pomodoro-tracking -gps "$(state)" -gpt "$(triggers)" -gpd "$(duration)" -gpe "$(elapsed)"
 ```
 
 <p align="center">  
  <img src="assets/gnome-pomodoro-settings.gif" width="400"/>
 </p>
 
+## Tests
+
+```bash
+
+# 1.- Run with mock values
+python -m unittest
 
-## Contributing
+# 2.- Run with Third-party response
+# Clockify
+export GP_TRACKING_CLOCKIFY_TOKEN=Token
+# Toggl
+export GP_TRACKING_TOGGL_TOKEN=Token
+# Odoo
+export GP_TRACKING_ODOO_URL=Url
+export GP_TRACKING_ODOO_PASSWORD=Password
+export GP_TRACKING_ODOO_USERNAME=User
+export GP_TRACKING_ODOO_DATABASE=Db
 
-[Guidelines](./CONTRIBUTING.md) 
+python -m unittest
 
-## Tests 
-[Guidelines](./tests/README.md)
\ No newline at end of file
+```
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
index e69de29..e6bbbd9 100644
--- a/__init__.py
+++ b/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
\ No newline at end of file
diff --git a/assets/clockify/README.md b/assets/clockify/README.md
index b68c5fc..34b6b66 100644
--- a/assets/clockify/README.md
+++ b/assets/clockify/README.md
@@ -4,31 +4,26 @@
 
 ![](token.png)
 
-* Add token 
+* Add token
 
   ```bash
-  gp-tracking --clockify-token TOKEN
+  gnome-pomodoro-tracking --clockify-token TOKEN
   ```
 
 * Workspaces
-  * List
-    ```bash
-    gp-tracking --clockify-workspaces
-    ```
 
-  * Set
-    ```bash
-    gp-tracking --clockify-workspaces --set ID
+  ```bash
+  # List
+  gnome-pomodoro-tracking --clockify-workspaces
+  # set 
+  gnome-pomodoro-tracking --clockify-workspaces --set ID
   ```
   
 * Projects
-  * List
-    ```bash
-    gp-tracking --clockify-projects
-    ```
 
-  * Set
   ```bash
-    gp-tracking --clockify-projects --set ID
-  ```
-
+  # List
+  gnome-pomodoro-tracking --clockify-projects
+  # Set 
+  gnome-pomodoro-tracking --clockify-projects --set ID
+  ```
\ No newline at end of file
diff --git a/assets/odoo/README.md b/assets/odoo/README.md
index 64b1ccd..039fd71 100644
--- a/assets/odoo/README.md
+++ b/assets/odoo/README.md
@@ -7,36 +7,33 @@
 ## Test 
 * ✅ Odoo 13.0 - Community
 * ✅ Odoo 13.0 - Enterprise
-* ❌ Odoo 12.0 - Community
-* ❌ Odoo 12.0 - Enterprise
+* ✅ Odoo 12.0 - Community
+* ✅ Odoo 12.0 - Enterprise
 * ❌ Odoo 11.0 - Community
 * ❌ Odoo 11.0 - Enterprise
 
 ## CLI
 
-*  Credentials
+* Credentials
+
   ```bash
-  gp-tracking --odoo-database DB --odoo-url URL  --odoo-username USER --odoo-password PASS
+  gnome-pomodoro-tracking --odoo-database DB --odoo-url URL  --odoo-username USER --odoo-password PASS
   ```
 
 * Projects
-  * List
-    ```bash
-    gp-tracking --odoo-projects
-    ```
-
-  * Set
-    ```bash
-    gp-tracking --odoo-projects --set ID
-    ```
+  
+  ```bash
+  # List
+  gnome-pomodoro-tracking --odoo-projects
+  # Set
+  gnome-pomodoro-tracking --odoo-projects --set ID
+  ```
 
 * Tasks
-  * List
-    ```bash
-    gp-tracking --odoo-tasks
-    ```
-
-  * Set
-    ```bash
-    gp-tracking --odoo-tasks --set ID
-    ```
\ No newline at end of file
+
+  ```bash
+  # List
+  gnome-pomodoro-tracking --odoo-tasks
+  # Set
+  gnome-pomodoro-tracking --odoo-tasks --set ID
+  ```
diff --git a/assets/toggl/README.md b/assets/toggl/README.md
index 7eabf24..f5c66ad 100644
--- a/assets/toggl/README.md
+++ b/assets/toggl/README.md
@@ -1,34 +1,29 @@
-# toggl 
+# toggl
 
 * Generate token API in https://track.toggl.com/profile
 
 ![](token.png)
 
-* Add token 
+* Add token
 
   ```bash
-  gp-tracking --toggl-token TOKEN
+  gnome-pomodoro-tracking --toggl-token TOKEN
   ```
 
 * Workspaces
-  * List
-    ```bash
-    gp-tracking --toggl-workspaces
-    ```
-
-  * Set
-    ```bash
-    gp-tracking --toggl-workspaces --set ID
+
+  ```bash
+  # List
+  gnome-pomodoro-tracking --toggl-workspaces
+  # Set 
+  gnome-pomodoro-tracking --toggl-workspaces --set ID
   ```
-  
+
 * Projects
-  * List
-    ```bash
-    gp-tracking --toggl-projects
-    ```
 
-  * Set
   ```bash
-    gp-tracking --toggl-projects --set ID
+  # List 
+  gnome-pomodoro-tracking --toggl-projects
+  # Set 
+  gnome-pomodoro-tracking --toggl-projects --set ID
   ```
-
diff --git a/gnome-pomodoro-tracking.py b/gnome-pomodoro-tracking.py
index b9c3c69..ead17a5 100755
--- a/gnome-pomodoro-tracking.py
+++ b/gnome-pomodoro-tracking.py
@@ -1,5 +1,6 @@
 #! /usr/bin/python3
-import os 
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
+import os
 from gp_tracking import GPTracking
 
 DIRHOME = os.path.expanduser("~")
@@ -9,7 +10,7 @@
     if not os.path.exists(GPT_CONF):
         print( "The file {} not exists. Try re-install" .format(GPT_CONF))
     else:
-        gpt = GPTracking(GPT_CONF, "", DIRHOME)
+        gpt = GPTracking(GPT_CONF)
         gpt.load_plugin()
         gpt.add_parse_args()
         gpt.cli()
diff --git a/gnome-pomodoro-tracking.template b/gnome-pomodoro-tracking.template
index d72a54e..eb86b6e 100644
--- a/gnome-pomodoro-tracking.template
+++ b/gnome-pomodoro-tracking.template
@@ -1,8 +1,16 @@
 [settings]
-plugin =
+plugin = 
 
 [pomodoro]
-type =
-name =
-start =
-end =
\ No newline at end of file
+type = 
+name = 
+start = 
+end = 
+
+[toggl]
+
+[clockify]
+
+[odoo]
+
+
diff --git a/gp_tracking.py b/gp_tracking.py
index 4b2c168..7a330f3 100644
--- a/gp_tracking.py
+++ b/gp_tracking.py
@@ -1,308 +1,269 @@
-import os 
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
+import os
 import argparse
 import configparser
 import sys
 from datetime import datetime, timedelta
 import logging
-
-
-DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
+from plugins import gpt_utils as utils
 
 class GPTracking:
 
-    def __init__(self, gptconfig, dirpath, dirhome):
-        self.gptconfig = gptconfig
-        self.dirpath = os.path.dirname(os.path.realpath(__file__))
-        self.dirhome = dirhome
+    def __init__(self, config_path):
+        self.config_path = config_path
+        self.code_path = os.path.dirname(os.path.realpath(__file__))
 
         self.config = configparser.RawConfigParser()
-        self.config.read(self.gptconfig)
+        self.config.read(self.config_path)
         self.parse = argparse.ArgumentParser(
-            prog="gp-tracking",
-            description='''
-            It is a custom action for Gnome Pomodoro, 
-            that connect any Time Tracking Software and create Time Entries.
-            ''',
-            epilog="Enjoy the program Gnome Pomodoro Tracking.",
+            prog="gnome-pomodoro-tracking",
+            description='''Lets you track your time with the popular time tracking services''',
+            epilog="GONME Pomodoro Tracking <https://gnomepomodoro.org>",
         )
         self.plugin = None
         self.logger = logging.getLogger(__name__)
-    
-    def gptconfig_set_section(self, section):
+
+    def add_section_config(self, section):
         self.config.add_section(section)
-        self.gptconfig_write()
+        self._write_config()
 
-    def gptconfig_set(self, section, key, value):
+    def set_config(self, section, key, value):
         self.config.set(section, key, value)
-        self.gptconfig_write()
+        self._write_config()
 
-    def gptconfig_get(self, section, key):
+    def get_config(self, section, key):
         return self.config.get(section, key)
 
-
-    def gptconfig_write(self):
-        with open(self.gptconfig, "w") as f: 
+    def _write_config(self):
+        with open(self.config_path, "w") as f:
             self.config.write(f)
 
-    def gptconfig_settings(self, key, value=None):
+    def settings_config(self, key, value=None):
         if value:
             self.config.set("settings", key, value)
-            self.gptconfig_write()
+            self._write_config()
         else:
             return self.config.get("settings", key)
 
-    def gptconfig_pomodoro(self, key, value=None):
+    def pomodoro_config(self, key, value=None):
         if value:
             self.config.set("pomodoro", key, value)
-            self.gptconfig_write()
+            self._write_config()
         else:
             return self.config.get("pomodoro", key)
 
-    def gptconfig_pomodoro_clean(self):
-        for k in ["start","end", "type",'name']: 
+    def pomodoro_config_clean(self):
+        for k in ["start", "end", "type", 'name']:
             self.config.set("pomodoro", k, "")
-        self.gptconfig_write()
-    
-    @classmethod
-    def today (cls, dt=None):        
-        return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
-    
-    def convert2minutes(self, seconds):
-        return seconds/60.0
-
-    def diff_elapsed(self, start, end):
-        try:
-            dt_start = datetime.strptime(start, "%Y-%m-%dT%H:%M:%SZ")
-            dt_end = datetime.strptime(end, "%Y-%m-%dT%H:%M:%SZ")
-            elapsed = dt_end - dt_start
-            return elapsed.total_seconds()
-        except Exception as e:
-            pass
-        return 0
-        
-        
-
-    @classmethod
-    def exists(cls, path, msg="does not exist"):
-        return os.path.exists(path)
-
-    @classmethod
-    def exit(cls, msg, code = 0):
-        print(msg)
-        exit(code)
-    
-    
-    #@classmethod
-    #def dirpath(cls):
-    #    return os.path.dirname(os.path.realpath(__file__))
+        self._write_config()
+
+    def logger_config(self):
+        formatter = logging.Formatter(
+            "%(asctime)s %(levelname)-8s %(filename)s:%(funcName)s %(message)s")
+
+        fileh = logging.FileHandler('.gnome-pomodoro-tracking.log', 'a')
+        fileh.setFormatter(formatter)
+        self.logger.addHandler(fileh)
+
+        class StreamToLogger(object):
+            def __init__(self, logger, log_level=logging.INFO):
+                self.logger = logger
+                self.log_level = log_level
+                self.linebuf = ''
+
+            def write(self, buf):
+                for line in buf.rstrip().splitlines():
+                    self.logger.log(self.log_level, line.rstrip())
+
+            def flush(self):
+                pass
+
+        sys.stdout = StreamToLogger(self.logger, logging.INFO)
+        sys.stderr = StreamToLogger(self.logger, logging.ERROR)
+
+        self.logger.setLevel(logging.DEBUG)
 
     def load_plugin(self):
 
         try:
-            plugin = self.gptconfig_settings("plugin") 
+            plugin = self.settings_config("plugin")
 
             if not len(plugin):
-              raise configparser.NoOptionError("plugin", "settings")
-            path_plugin = "%s/plugins/%s.py" % (self.dirpath, plugin)
-            if not os.path.isfile(path_plugin) :
+                raise configparser.NoOptionError("plugin", "settings")
+            path_plugin = "%s/plugins/%s.py" % (self.code_path, plugin)
+            if not os.path.isfile(path_plugin):
                 raise configparser.NoOptionError("plugin", "settings")
 
-            globals()[plugin] = __import__("plugins.%s"% plugin)            
+            globals()[plugin] = __import__("plugins.%s" % plugin)
             pluginClass = getattr( getattr(globals()[plugin], plugin), plugin.title())
-            __import__("plugins.%s"% plugin)            
+            __import__("plugins.%s" % plugin)
             self.plugin = pluginClass(self)
-            
             return True
-        except configparser.NoSectionError as e :
-            self.logger.critical("In the file .gp-tracking.conf no exists secction [settings].\nTry re-install for regenerate file config.\n")
-        except configparser.NoOptionError as e:            
-            self.logger.critical("Exec the command gp-tracking --plugin NAME for set.\n")
+        except configparser.NoSectionError as e:
+            self.logger.critical("In the file .gnome-pomodoro-tracking.conf no exists secction [settings]."
+                                 "\nTry re-install for regenerate file config.\n")
+        except configparser.NoOptionError as e:
+            self.logger.critical("Exec the command gnome-pomodoro-tracking --plugin NAME for set.\n")
         except ModuleNotFoundError as e:
-            self.logger.critical("Fail load the plugin module. Exec the command gp-tracking --plugin NAME for  replace.\n")
-        except Exception as e :
+            self.logger.critical("Fail load the plugin module. Exec the command"
+                                 "gnome-pomodoro-tracking --plugin NAME for  replace.\n")
+        except Exception as e:
             self.logger.critical(e)
 
         return False
 
-    def gptparse_params(self):
-        return self.parse.parse_args()
-
     def add_parse_args(self):
-        
         self.parse.add_argument('--plugin',
-            action='store', 
-            dest='plugin', 
-            help='Select Time Tracking Software', 
-            choices= ['odoo', 'clockify', 'toggl'],
-        )
-    
-        self.parse.add_argument('-gps','--gp-state',
-            action='store', 
-            dest='gp_state', 
-            choices= ['pomodoro', 'short-break', 'long-break'],
-            help=argparse.SUPPRESS)
+                                action='store',
+                                dest='plugin',
+                                help='Select Time Tracking Service',
+                                choices=['odoo', 'clockify', 'toggl'])
+
+        self.parse.add_argument('-gps', '--gp-state',
+                                action='store',
+                                dest='gp_state',
+                                choices=['pomodoro', 'short-break', 'long-break'],
+                                help=argparse.SUPPRESS)
         self.parse.add_argument('-gpt', '--gp-trigger',
-            action='store', 
-            dest='gp_trigger', 
-            help=argparse.SUPPRESS)
+                                action='store',
+                                dest='gp_trigger',
+                                help=argparse.SUPPRESS)
         self.parse.add_argument('-gpd', '--gp-duration',
-            action='store', 
-            dest='gp_duration', 
-            help=argparse.SUPPRESS)
+                                action='store',
+                                dest='gp_duration',
+                                help=argparse.SUPPRESS)
         self.parse.add_argument('-gpe', '--gp-elapsed',
-            action='store', 
-            dest='gp_elapsed', 
-            help=argparse.SUPPRESS)
+                                action='store',
+                                dest='gp_elapsed',
+                                help=argparse.SUPPRESS)
 
         self.parse.add_argument('-n', '--name',
-            action='store',
-            dest='name', 
-            help='''
-                Set the name of the time entry (Pomodoro, short/long break). 
-                If there is no active Pomodoro start a new one.
-            ''')
+                                action='store',
+                                dest='name',
+                                help='''Set the name of the time entry''')
         self.parse.add_argument('-r', '--restart',
-            action='store_const',
-            dest='reset', 
-            const=True,
-            help='Stop the Pomodoro & starts a new')
+                                action='store_const',
+                                dest='reset',
+                                const=True,
+                                help='Restart Pomodoro')
         self.parse.add_argument('-k', '--stop',
-            action='store_const',
-            dest='stop', 
-            const=True,
-            help='Stop the Pomodoro')
+                                action='store_const',
+                                dest='stop',
+                                const=True,
+                                help='Stop Pomodoro')
         self.parse.add_argument('-s', '--status',
-            action='store_const',
-            dest='status', 
-            const=True,
-            help='Displays the summary of the Pomodoro')
-        
-        self.parse.add_argument('--set', 
-            action='store',
-            dest='set', 
-            help=argparse.SUPPRESS)
-        
-        self.parse.add_argument('--test-time-entry', 
-            action='store_const',
-            dest='test_time_entry', 
-            const=True,
-            help=argparse.SUPPRESS)
-
-        if getattr(self.plugin, 'add_parse_args',False):
-            getattr(self.plugin, 'add_parse_args')(kind="optionals")
-    
-    def print_cli(self, items, **kwargs):
-        title = kwargs.get('title', "")
-        if len(title):
-            print("-" *  len(title))
-            print(title)
-            print("-" *  len(title))
-        lines = ""
-        for item in items:
-            line = ""
-            for k in item.keys():
-                line += "| %s " %item.get(k)
-            print(line)
-            lines += line +"\n"
-        
-        if str(os.getenv("GP_TRACKING_ENV", "")).lower() == 'test':
-            original_stdout = sys.stdout
-            with open(self.dirpath + '/tests/stdout.txt', 'w') as f:
-                sys.stdout = f
-                print(title +"\n" +lines)
-                sys.stdout = original_stdout 
-
-    """
-        Gnome pomodoro methods
-    """
+                                action='store_const',
+                                dest='status',
+                                const=True,
+                                help='Status Pomodoro')
+        self.parse.add_argument('-d', '--debug',
+                                action='store_const',
+                                dest='debug',
+                                const=True,
+                                help=argparse.SUPPRESS)
+        self.parse.add_argument('--set',
+                                action='store',
+                                dest='set',
+                                help=argparse.SUPPRESS)
+        self.parse.add_argument('--time-entry',
+                                action='store_const',
+                                dest='time_entry',
+                                const=True,
+                                help=argparse.SUPPRESS)
+
+        if getattr(self.plugin, 'add_parse_args', False):
+            getattr(self.plugin, 'add_parse_args')(None)
+
     def cli(self):
-        params = self.gptparse_params() 
-        
-        if self.gnome_pomodoro():
-            return
-
-        if params.plugin:
-            plugin = self.gptconfig_settings("plugin")
-            self.gptconfig_settings("plugin", params.plugin)
-            if not self.load_plugin():            
-                self.gptconfig_settings("plugin", plugin)
-
-        if params.reset or params.stop:
-            params.trigger = 'skip'
-            params.duration = "0"
-            params.elapsed = "0"
-            self.gnome_pomodoro(params=params)
-            os.system("gnome-pomodoro --stop")
-            if params.reset:
-                os.system("gnome-pomodoro --start --no-default-window")
-        if params.name:
-            if not len(self.gptconfig_pomodoro("name")) and not len(self.gptconfig_pomodoro("start")):            
+        params = self.parse.parse_args()
+        if params.debug:
+            self.logger_config()
+
+        if not self.gnome_pomodoro():
+            if params.plugin:
+                plugin = self.settings_config("plugin")
+                self.settings_config("plugin", params.plugin)
+                if not self.load_plugin():
+                    self.settings_config("plugin", plugin)
+
+            if params.reset or params.stop:
+                params.trigger = 'skip'
+                params.duration = "0"
+                params.elapsed = "0"
+                self.gnome_pomodoro(params=params)
                 os.system("gnome-pomodoro --stop")
-                os.system("gnome-pomodoro --start --no-default-window")
-            self.gptconfig_pomodoro("name", params.name)
-        if params.status:
-            items = []
-            dt_start = self.today()
-            for k in ["type", "name", "start"]:
-                try:                    
-                    if k == 'start' : dt_start = self.gptconfig_pomodoro(k) 
-                    items.append({'name': "%s: %s" % ( str(k).title(), self.gptconfig_pomodoro(k) )})                    
-                except: 
-                    pass
-            items.append({'name': 'Elapsed: {0:.2f} Min'.format(self.convert2minutes(self.diff_elapsed(dt_start, self.today() ))) })
-            
-            self.print_cli(items, title="Gnome Pomodoro Tracking - %s" % self.gptconfig_settings("plugin"))
-            if getattr(self.plugin, 'status',False):
-                getattr(self.plugin, 'status')()
-
-        if params.test_time_entry:
-            if getattr(self.plugin, 'add_time_entry',False):
-                end = datetime.utcnow()
-                start = end - timedelta(minutes=25)
-                result = getattr(self.plugin, 'add_time_entry')(
-                    description= "Test Time entry ", 
-                    start=start.strftime(DATETIME_FORMAT), 
-                    end=end.strftime(DATETIME_FORMAT),
-                    minutes= 25,
-                )
-
-        if getattr(self.plugin, 'cli',False):
+                if params.reset:
+                    os.system("gnome-pomodoro --start --no-default-window")
+            if params.name:
+                if not len(self.pomodoro_config("name")) and not len(self.pomodoro_config("start")):
+                    os.system("gnome-pomodoro --stop")
+                    os.system("gnome-pomodoro --start --no-default-window")
+                self.pomodoro_config("name", params.name)
+            if params.status:
+                items = [
+                    {'key': 'Plugin', 'value': str(self.settings_config("plugin")).title()}
+                ]
+                dt_start = utils.now()
+                for k in ["name", "type", 'start']:
+                    try:
+                        if k == 'start':
+                            dt_start = self.pomodoro_config(k)
+                        else:
+                            items.append({'key': str(k).title(), 'value': self.pomodoro_config(k)})
+                    except Exception:
+                        pass
+                items.append({'key': 'Elapsed', 'value': '{0:.2f} Min'.format(
+                    utils.time_elapsed(dt_start, utils.now(), formatter='minutes')) })
+                utils.printtbl(items)
+                if getattr(self.plugin, 'status', False):
+                    getattr(self.plugin, 'status')()
+
+            if params.time_entry:
+                if getattr(self.plugin, 'add_time_entry', False):
+                    end = datetime.utcnow()
+                    start = end - timedelta(minutes=25)
+                    result = getattr(self.plugin, 'add_time_entry')(
+                        name="Time entry",
+                        start=start.strftime(utils.DATETIME_FORMAT),
+                        end=end.strftime(utils.DATETIME_FORMAT),
+                        minutes=25,
+                    )
+                    utils.printtbl([result])
+
+            if getattr(self.plugin, 'cli', False):
                 getattr(self.plugin, 'cli')()
 
-    def gnome_pomodoro(self, params=None):        
+    def gnome_pomodoro(self, params=None):
         """
-           gp-tracking -gps $(state) -gpt "$(triggers)" -gpd $(duration) -gpe $(elapsed)
+           gnome-pomodoro-tracking -gps $(state) -gpt "$(triggers)" -gpd $(duration) -gpe $(elapsed)
         """
-        params = self.gptparse_params() if not params else params
-        for p in ['gp_state', 'gp_trigger','gp_duration','gp_elapsed']:
+        params = self.parse.parse_args() if not params else params
+        for p in ['gp_state', 'gp_trigger', 'gp_duration', 'gp_elapsed']:
             if not getattr(params, p, False):
-                return False        
-        #Start timer
-        if 'start' in params.gp_trigger or 'resume' in params.gp_trigger:          
-            self.gptconfig_pomodoro("type", params.gp_state.title())
-            self.gptconfig_pomodoro("start", self.today())
+                return False
+        # Start timer
+        if 'start' in params.gp_trigger or 'resume' in params.gp_trigger:
+            self.pomodoro_config("type", params.gp_state.title())
+            self.pomodoro_config("start", utils.now())
         # Stop timer
         elif 'skip' in params.gp_trigger or 'pause' in params.gp_trigger or 'complete' in params.gp_trigger:
             try:
-                name = self.gptconfig_pomodoro("name") or self.gptconfig_pomodoro("type")
-                start = self.gptconfig_pomodoro("start")            
-                end = self.today()
-                minutes = self.convert2minutes(self.diff_elapsed(start, end))            
-                if minutes > 0:                                 
-                    if getattr(self.plugin, 'add_time_entry',False):
-                        # Check param --test-time-entry
-                        add_time_entry = getattr(self.plugin, 'add_time_entry')(
+                name = self.pomodoro_config("name") or self.pomodoro_config("type")
+                start = self.pomodoro_config("start")
+                end = utils.now()
+                minutes = utils.time_elapsed(start, end, formatter='minutes')
+                if minutes > 0:
+                    if getattr(self.plugin, 'add_time_entry', False):
+                        result = getattr(self.plugin, 'add_time_entry')(
                             name=name,
-                            start=start, 
+                            start=start,
                             end=end,
-                            minutes= minutes,
-                        )
-                        if add_time_entry:
-                            self.logger.info("The time entry is added successfully %s " % add_time_entry)
-                        self.gptconfig_pomodoro_clean()
-                else: 
-                    self.gptconfig_pomodoro_clean()
+                            minutes=minutes)
+                        utils.printtbl([result])
+                        self.pomodoro_config_clean()
+                else:
+                    self.pomodoro_config_clean()
             except Exception as e:
                 print(e)
         return True
-
diff --git a/plugins/__init__.py b/plugins/__init__.py
index e69de29..e6bbbd9 100644
--- a/plugins/__init__.py
+++ b/plugins/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
\ No newline at end of file
diff --git a/plugins/clockify.py b/plugins/clockify.py
index 1a6ba5d..d84c389 100644
--- a/plugins/clockify.py
+++ b/plugins/clockify.py
@@ -1,216 +1,198 @@
-import json
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import configparser
-import logging
-
 from .gpt_plugin import GPTPlugin
-from .gpt_utils import join_url
-
-logger = logging.getLogger(__name__)
+from .gpt_utils import join_url, printtbl, config_attrs,\
+    find_by_id, only_columns
 
 class Clockify(GPTPlugin):
     name = "clockify"
     url = "https://api.clockify.me"
-    
+
     def __init__(self, gpt):
         super().__init__(gpt)
-            
+
     def setup(self):
         try:
-            self.session.update({"token": self.gpt.gptconfig_get(self.name, "token")})
+            self.session.update({"token": self.gpt.get_config(self.name, "token")})
         except configparser.NoSectionError as e:
-            #logger.error(str(e))
-            self.gpt.gptconfig_set_section(self.name)
+            self.gpt.logger.error(e)
+            self.gpt.add_section_config(self.name)
             self.add_parse_args(kind="setup-args")
-        except configparser.NoOptionError as e:            
-            #logger.error(str(e))
+        except configparser.NoOptionError as e:
+            self.gpt.logger.error(e)
             self.add_parse_args(kind="setup-args")
-            params =  self.gpt.gptparse_params()
+            params = self.gpt.parse.parse_args()
             self.session.update({"token": params.clockify_token})
             try:
                 if self.auth():
-                    self.gpt.gptconfig_set(self.name, "token", self.token())
+                    self.gpt.set_config(self.name, "token", self.token())
                     print(f"{self.name} now can do you use.")
             except Exception as e:
-                logger.error(str(e))
-                exit(0)        
-    
-    token = lambda self: self.session.get("token", "")
-    http_headers = lambda self: {'X-Api-Key': self.token()}
+                self.gpt.logger.critical(e)
+                exit(0)
+
+    def token(self):
+        return self.session.get("token", "")
+
+    def http_headers(self):
+        return{'X-Api-Key': self.token()}
 
     def add_parse_args(self, kind):
         if kind == "setup-args":
             self.gpt.parse.add_argument('--clockify-token',
-                action='store', 
-                dest='clockify_token', 
-                help=' e.g XtGTMKadTS8sJ/E', 
-                required=True
-            )
+                                        action='store',
+                                        dest='clockify_token',
+                                        help=' e.g XtGTMKadTS8sJ/E',
+                                        required=True)
         else:
-        # Overwrite
-            self.gpt.parse.add_argument('--clockify-workspaces', 
-                    action='store_const', 
-                    dest='clockify_workspaces', 
-                    help='List clockify workspaces',     
-                    const=True,                
-                    )
-            self.gpt.parse.add_argument('--clockify-projects', 
-                    action='store_const', 
-                    dest='clockify_projects', 
-                    help='List clockify projects',     
-                    const=True,
-                    )
-    
+            # Overwrite
+            self.gpt.parse.add_argument('--clockify-workspaces',
+                                        action='store_const',
+                                        dest='clockify_workspaces',
+                                        help='List clockify workspaces',
+                                        const=True)
+            self.gpt.parse.add_argument('--clockify-projects',
+                                        action='store_const',
+                                        dest='clockify_projects',
+                                        help='List clockify projects',
+                                        const=True)
+
     def auth(self):
-        url = join_url(self.url, "/api/v1/user")
+        url = join_url(self.url, "api/v1/user")
         try:
-            data = self.http_call('GET', url, headers= self.http_headers())
-            return data.get("id") != ""
-        except Exception as e :
-            msg =""
-            try:
-                err = json.loads(str(e))
-                msg = err.get("message", "Fail Auth")
-            except Exception as jl:
-                pass
-            raise Exception(msg)
-
+            req = self.rget(url, headers=self.http_headers())
+            if req.ok:
+                data = req.json()
+                return data.get("id") != ""
+            else:
+                raise Exception(req.text)
+        except Exception as e:
+            self.gpt.logger.exception(e)
         return False
-    
-    def cli(self):
-        # Overwrite        
-        params = self.gpt.gptparse_params() 
-        def findbyid(rows, id):
-            for row in rows:
-                for k in row.keys():
-                    if k == 'id' and row.get(k) == id:
-                        return row
-            return None
 
-        def onlycolumns(rows):
-            l = []
-            for r in rows: 
-                l.append( { 'id': r.get('id'), 'name':  r.get('name')})
-            return l
-        
-        
+    def cli(self):
+        # Overwrite
+        params = self.gpt.parse.parse_args()
 
-        if params.clockify_workspaces:
-            try:              
+        if hasattr(params, 'clockify_workspaces') and params.clockify_workspaces:
+            try:
                 rows = self.workspaces()
                 if rows:
-                    rows = onlycolumns(rows)
-                    title ="Clockify workspaces"
+                    rows = only_columns(rows)
                     if params.set:
-                        row = findbyid(rows, params.set)
-                        if row: 
-                            self.gpt.gptconfig_set(self.name, "workspace_id",row.get('id') )
-                            self.gpt.gptconfig_set(self.name, "workspace_name",row.get('name') )
-                            self.gpt.print_cli([], title= 'the workspace was added successfully')
+                        row = find_by_id(rows, params.set)
+                        if row:
+                            self.gpt.set_config(self.name, "workspace_id", row.get('id'))
+                            self.gpt.set_config(self.name, "workspace_name", row.get('name'))
+                            printtbl([row])
                         else:
-                            self.gpt.print_cli([], title= 'the workspace id was not found')
+                            print('The workspace ID was not found')
                     else:
-                        self.gpt.print_cli(rows, title=title)
+                        printtbl(rows)
                 else:
                     raise Exception("Fail get workspaces")
             except Exception as e:
-                self.gpt.exit(e)
-        elif params.clockify_projects:
+                self.gpt.logger.exception(e)
+        elif hasattr(params, 'clockify_projects') and params.clockify_projects:
             try:
-                workspace_id = self.gpt.gptconfig_get(self.name, "workspace_id")
+                workspace_id = self.gpt.get_config(self.name, "workspace_id")
             except Exception as e:
-                #logger.error(e)
+                self.gpt.logger.error(e)
                 workspace = self.workspaces(filter='first')
                 workspace_id = workspace.get('id')
             try:
-                rows = self.projects(workspace_id)                
+                rows = self.projects(workspace_id)
                 if rows:
-                    rows = onlycolumns(rows)
-                    title ="Clockify projects"
+                    rows = only_columns(rows)
                     if params.set:
-                        row = findbyid(rows, params.set)
-                        if row: 
-                            self.gpt.gptconfig_set(self.name, "project_id",row.get('id') )
-                            self.gpt.gptconfig_set(self.name, "project_name",row.get('name') )
-                            self.gpt.print_cli([], title= 'the project was added successfully')
+                        row = find_by_id(rows, params.set)
+                        if row:
+                            self.gpt.set_config(self.name, "project_id", row.get('id'))
+                            self.gpt.set_config(self.name, "project_name", row.get('name'))
+                            printtbl([row])
                         else:
-                            self.gpt.print_cli([], title= 'the project id was not found')
-                    else: 
-                        self.gpt.print_cli(rows, title=title)
+                            print('The project ID was not found')
+                    else:
+                        printtbl(rows)
                 else:
                     raise Exception("Fail get projects")
             except Exception as e:
-                raise Exception(e)
-    
+                self.gpt.logger.exception(e)
+
     def workspaces(self, filter=""):
-        url = join_url(self.url, "/api/v1/workspaces")
+        url = join_url(self.url, "api/v1/workspaces")
         try:
-            data = self.http_call('GET', url, headers = self.http_headers())
-            if filter =='first':
-                if data:
-                    return len(data) and data[0]
-            return data
-        except:
-            pass
+            req = self.rget(url, headers=self.http_headers())
+            if req.ok:
+                data = req.json()
+                self.gpt.logger.info(data)
+                if filter == 'first':
+                    if data:
+                        return len(data) and data[0]
+                return data
+            else:
+                raise Exception(req.text)
+        except Exception as e:
+            self.gpt.logger.exception(e)
         return None
 
     def projects(self, workspace_id, filter=""):
-        url =join_url(self.url,f"/api/v1/workspaces/{workspace_id}/projects")
-        try:            
-            data = self.http_call('GET', url, headers = self.http_headers())
-            if filter =='first' :            
-                return len(data) and data[0]
-            return data
-        except:
-            pass
+        url = join_url(self.url, f"api/v1/workspaces/{workspace_id}/projects")
+        try:
+            req = self.rget(url, headers=self.http_headers())
+            if req.ok:
+                data = req.json()
+                self.gpt.logger.info(data)
+                if filter == 'first':
+                    return len(data) and data[0]
+                return data
+            else:
+                raise Exception(req.text)
+        except Exception as e:
+            self.gpt.logger.exception(e)
         return None
-    
-    def add_time_entry(self, **kwargs ):
+
+    def add_time_entry(self, **kwargs):
         # Overwrite
-        description = kwargs.get('name')
-        start= kwargs.get('start')
-        end= kwargs.get('end')
+        name = kwargs.get('name')
+        start = kwargs.get('start')
+        end = kwargs.get('end')
 
         workspace_id  = ""
         try:
-            workspace_id = self.gpt.gptconfig_get(self.name, "workspace_id")
-        except:
+            workspace_id = self.gpt.get_config(self.name, "workspace_id")
+        except Exception:
             try:
                 workspace = self.workspaces(filter='first')
                 workspace_id = workspace.get('id')
-            except:
+            except Exception:
                 pass
         project_id = None
         try:
-            project_id = self.gpt.gptconfig_get(self.name, "project_id")
-        except:
-            pass        
+            project_id = self.gpt.get_config(self.name, "project_id")
+        except Exception:
+            pass
         time_entry = {
-            "start": start, # Required 
-            "description": description,
+            "start": start,  # Required
+            "description": name,
             "projectId": project_id,
-            "end": end, # Required
+            "end": end,  # Required
         }
         try:
             url = join_url(self.url, f"api/v1/workspaces/{workspace_id}/time-entries")
-            data = self.http_call('POST', url,json= time_entry, headers= self.http_headers())
-            return data["id"]
+            req = self.rpost(url, json=time_entry, headers=self.http_headers())
+            if req.ok:
+                data = req.json()
+                self.gpt.logger.info(data)
+                return {'id': data['id'], 'name': name}
+            else:
+                raise Exception(req.text)
         except Exception as e:
-            pass
-        return -1
-    
-    def status(self, **kwargs):
-        # Overwrite
-        items = []
-        def getstate(param):
-            try:
-                id = self.gpt.gptconfig_get(self.name, param+"_id")
-                name =self.gpt.gptconfig_get(self.name, param+"_name")
-                if len(id) and len(name):
-                    items.append({'name': "%s: %s - %s " % (str(param).title(), id, name)})
-            except:
-                pass
-        getstate('workspace')
-        getstate('project')
-        self.gpt.print_cli(items)
+            self.gpt.logger.exception(e)
+        return None
 
-    
\ No newline at end of file
+    def status(self, **kwargs):
+        attrs = ['workspace_name', 'project_name']
+        items = config_attrs(self.gpt, self.name, attrs, formatter='status')
+        printtbl(items)
\ No newline at end of file
diff --git a/plugins/gpt_plugin.py b/plugins/gpt_plugin.py
index ee0d3ff..ef3bd48 100644
--- a/plugins/gpt_plugin.py
+++ b/plugins/gpt_plugin.py
@@ -1,8 +1,9 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import requests
 
-
 class GPTPlugin(object):
-    
+
     name = None
     session = {}
 
@@ -10,7 +11,7 @@ def __init__(self, gpt):
         self.gpt = gpt
         self.setup()
 
-    #Management setup
+    # Management setup
     def setup(self):
         """
         Checks params required for provider connect.
@@ -20,23 +21,23 @@ def setup(self):
             None
         """
         raise NotImplementedError
-    
+
     def auth(self) -> bool:
         """
-        Check if auth was successful 
+        Check if auth was successful
         """
         raise NotImplementedError
 
-    # Management Time Entry 
-    def add_time_entry(self, **kwargs):
+    # Management Time Entry
+    def add_time_entry(self, **kwargs) -> any:
         """
-        Params: 
+        Params:
             description,str: Description activity
             start,str:       Datetime in UTC format %Y-%m-%dT%H:%M:%SZ
             ends,str:        Datetime in UTC format %Y-%m-%dT%H:%M:%SZ
             minutes,float:   Elapsed
         return:
-            id,int:
+            dict: {'id': '1', 'name': 'Name'}
         Exception:
             fail to add
         """
@@ -44,46 +45,44 @@ def add_time_entry(self, **kwargs):
 
     def rm_time_entry(self, **kwargs) -> bool:
         """
-        Remove time entry in Time Tracking Software 
+        Remove time entry in Time Tracking Software
         Params:
             id,int: time entry | required
         Return:
             success,bool:
-        Exption: 
+        Exption:
             fail to remove
         """
         raise NotImplementedError
-    
+
     # Management CLI
     def add_parse_args(self, kind):
-        """ 
+        """
         Add optional arguments in command terminal
-        params: 
+        params:
             None
-        return: 
+        return:
             None
-        
         e.g
-        
-        self.gptracking.parse.add_argument('--clockify-workspaces', 
-            action='store_const', 
-            dest='clockify_workspaces', 
-            help='List clockify workspaces',     
-            const=True,                
+        self.gptracking.parse.add_argument('--clockify-workspaces',
+            action='store_const',
+            dest='clockify_workspaces',
+            help='List clockify workspaces',
+            const=True,
         )
         """
         raise NotImplementedError
-    
+
     def cli(self):
         """
         Interprets command line arguments
-        params: 
+        params:
             None
-        return: 
+        return:
             None
-        
+
         e.g
-        params = self.gptracking.gptparse_params()
+        params = self.gpt.parse.parse_args()
         if params.clockify_workspaces:
             pass
         """
@@ -92,27 +91,34 @@ def cli(self):
     def status(self):
         """
         Print the current state of the Pomodoro ( gp-tracking --status )
-        params: 
+        params:
             None
-        return: 
+        return:
             None
 
-        e.g 
+        e.g
 
-        _name = self.gptracking.gptconfig_get(self.GTP_CONFIG, "clockify_name")
-        self.gptracking.print_cli([{'name': _name }])
+        _name = self.gptracking.get_config(self.GTP_CONFIG, "clockify_name")
+        printtbl([{'key': _key, 'value': _value }])
 
         """
         raise NotImplementedError
 
-    # Management Request 
-    def http_call(self,method, url, **kwargs):
-        #url = urljoin(self.url, url)
-        #import pdb; pdb.set_trace()
-        response = requests.request(method, url, **kwargs)
-        if response.ok:
-            return response.json()
-        text =response.text
-        raise Exception(text)
+    # Request Management
+    def request(self, method, url, **kwargs):
+        """
+            Make Request
+        """
+        return requests.request(method, url, **kwargs)
 
+    def rget(self, url, **kwargs):
+        """
+            Request GET
+        """
+        return self.request('GET', url, **kwargs)
 
+    def rpost(self, url, **kwargs):
+        """
+            Request POST
+        """
+        return self.request('POST', url, **kwargs)
\ No newline at end of file
diff --git a/plugins/gpt_utils.py b/plugins/gpt_utils.py
index de872f5..0b6921f 100644
--- a/plugins/gpt_utils.py
+++ b/plugins/gpt_utils.py
@@ -1,12 +1,98 @@
-import platform
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import re
+from datetime import datetime
+
+DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
 
 def join_url(url, *paths):
-    if platform.python_version()[:3] == '3.7':
-        from urllib.parse import urljoin
-        for path in paths:
-            url = urljoin(url, path)
-        return url
     for path in paths:
         url = re.sub(r'/?$', re.sub(r'^/?', '/', path), url)
     return url
+
+def printtbl(rows, header=False):
+
+    def val(v):
+        return "{:<12} ".format(v)
+
+    if len(rows) > 0 and header:
+        line = ""
+        for key in rows[0].keys():
+            line += val(str(key).title())
+        print(line)
+
+    for row in rows:
+        line = ""
+        for key in row.keys():
+            line += val(row.get(key))
+        print(line)
+
+def find_by_id(rows: list, id: str):
+    """
+        params:
+            rows: [{'id': 2},{'id': 1}]
+            id: 1
+        return:
+            None,dict
+    """
+    for row in rows:
+        for key in row.keys():
+            if key == 'id' and str(row.get(key)) == id:
+                return row
+    return None
+
+def only_columns(rows: list, colums: list = ['id', 'name']):
+    """
+        params:
+            rows: [{'id': 2, 'name': 'A'},{'id': 1, 'name': 'B'}]
+            id: 1
+        return:
+            list
+    """
+    new_rows = []
+    for row in rows:
+        new_row = {}
+        for column in colums:
+            new_row.update({ column: row.get(column, None)})
+        new_rows.append(new_row)
+    return new_rows
+
+def config_attrs(gpt, section: str, attrs: list, formatter=None):
+    """
+        params:
+            gpt: GPTracking
+            section: pomodoro
+            attrs: ['name', 'type']
+            formatter: status
+        return:
+            list
+    """
+    rows = []
+    for attr in attrs:
+        try:
+            val = gpt.config.get(section, attr)
+            if formatter is not None and formatter == 'status':
+                key = str(str(attr).split('_')[0]).title()
+                rows.append({'key': key, 'value': val})
+            else:
+                rows.append({'key': attr, 'value': val})
+        except Exception as e:
+            pass
+    return rows
+
+def time_elapsed(dtstart, dtend, formatter="seconds"):
+    try:
+        if isinstance(dtstart, str) and isinstance(dtend, str):
+            dt_start = datetime.strptime(dtstart, DATETIME_FORMAT)
+            dt_end = datetime.strptime(dtend, DATETIME_FORMAT)
+        elapsed = dt_end - dt_start
+        seconds = elapsed.total_seconds()
+        if formatter == 'minutes':
+            return seconds / 60.0
+        return seconds
+    except Exception as e:
+        pass
+    return 0
+
+def now():
+    return datetime.utcnow().strftime(DATETIME_FORMAT)
\ No newline at end of file
diff --git a/plugins/odoo.py b/plugins/odoo.py
index cb7d3bb..52fcd59 100644
--- a/plugins/odoo.py
+++ b/plugins/odoo.py
@@ -1,190 +1,179 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import xmlrpc.client
 from urllib.parse import urlparse
 import configparser
-import logging
 from datetime import datetime
 
-from .gpt_plugin import GPTPlugin 
-from .gpt_utils import join_url
-
-logger = logging.getLogger(__name__)
+from .gpt_plugin import GPTPlugin
+from .gpt_utils import printtbl, join_url, find_by_id, config_attrs
 
 class Odoo(GPTPlugin):
 
     name = "odoo"
-    
+
     def __init__(self, gpt):
         super().__init__(gpt)
-    
 
     def setup(self):
         try:
             self.session.update({
-                "username" : self.gpt.gptconfig_get(self.name, "username"),
-                "password" : self.gpt.gptconfig_get(self.name, "password"),
-                "url" : self.gpt.gptconfig_get(self.name, "url"),
-                "database" : self.gpt.gptconfig_get(self.name, "database"),
+                "username": self.gpt.get_config(self.name, "username"),
+                "password": self.gpt.get_config(self.name, "password"),
+                "url": self.gpt.get_config(self.name, "url"),
+                "database": self.gpt.get_config(self.name, "database"),
             })
 
             if not self.auth():
                 raise Exception("Fail auth check credentials")
         except configparser.NoSectionError as e:
-            #logger.error(e)
-            self.gpt.gptconfig_set_section(self.name)
+            self.gpt.logger.error(e)
+            self.gpt.add_section_config(self.name)
             self.add_parse_args("setup-args")
-        except configparser.NoOptionError as e:            
-            #logger.error(e)
+        except configparser.NoOptionError as e:
+            self.gpt.logger.error(e)
             self.add_parse_args("setup-args")
-            params = self.gpt.gptparse_params()
+            params = self.gpt.parse.parse_args()
             try:
                 self.session.update({
-                    "username" : params.odoo_username,
-                    "password" : params.odoo_password,
-                    "url" :  params.odoo_url,
-                    "database" : params.odoo_database
-                })                
+                    "username": params.odoo_username,
+                    "password": params.odoo_password,
+                    "url":  params.odoo_url,
+                    "database": params.odoo_database})
                 if self.auth():
                     for p in ['username', 'password', 'url', 'database']:
-                        self.gpt.gptconfig_set(self.name, p,self.session.get(p))
+                        self.gpt.set_config(self.name, p, self.session.get(p))
                     print(f"{self.name} now can do you use.")
             except Exception as e:
-                logger.error(str(e))                
-                exit(0)
-        except Exception as e :
-            raise Exception(str(e))
-        
+                self.gpt.logger.exception(e)
 
     def add_parse_args(self, kind):
-        if kind =="setup-args":
+        if kind == "setup-args":
             self.gpt.parse.add_argument('--odoo-username',
-                action='store', 
-                dest='odoo_username',
-                required=True
-            )
+                                        action='store',
+                                        dest='odoo_username',
+                                        required=True)
             self.gpt.parse.add_argument('--odoo-password',
-                action='store', 
-                dest='odoo_password',
-                required=True
-            )
+                                        action='store',
+                                        dest='odoo_password',
+                                        required=True)
             self.gpt.parse.add_argument('--odoo-url',
-                action='store', 
-                dest='odoo_url',
-                required=True
-            )        
+                                        action='store',
+                                        dest='odoo_url',
+                                        required=True)
             self.gpt.parse.add_argument('--odoo-database',
-                action='store', 
-                dest='odoo_database',
-                required=True
-            )
+                                        action='store',
+                                        dest='odoo_database',
+                                        required=True)
 
         else:
             # Overwrite
             self.gpt.parse.add_argument('--odoo-projects',
-                action='store_const', 
-                dest='odoo_projects', 
-                help='Odoo projects',     
-                const=True,                
-            )
+                                        action='store_const',
+                                        dest='odoo_projects',
+                                        help='Odoo projects',
+                                        const=True)
             self.gpt.parse.add_argument('--odoo-tasks',
-                action='store_const', 
-                dest='odoo_tasks', 
-                help='Odoo projects/tasks',     
-                const=True,
-            )
-
+                                        action='store_const',
+                                        dest='odoo_tasks',
+                                        help='Odoo projects/tasks',
+                                        const=True)
 
     def auth(self):
         up = urlparse(self.session.get('url'))
         url = '{}://{}'.format( up.scheme or 'https', up.netloc)
         self.session.update({"url": url})
         uid = self.common().authenticate(
-            self.session.get('database'), 
-            self.session.get('username'), 
-            self.session.get('password'), {}
-        )
+            self.session.get('database'),
+            self.session.get('username'),
+            self.session.get('password'), {})
         self.session.update({"uid": uid})
-        return self.session.get("uid","") != ""
+        return self.session.get("uid", "") != ""
+
+    # Odoo operations
+    def version(self):
+        return self.common().version().get('server_version')
 
-    
-    # Odoo operations 
-    version = lambda self: self.common().version().get('server_version')
+    def common(self):
+        return xmlrpc.client.ServerProxy(join_url(self.session.get("url", ""), 'xmlrpc/2/common'))
 
-    common = lambda self: xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(self.session.get("url","")))
-  
-   
-    
     def models(self, model, method, domain, options=False):
         if not self.session.get("uid"):
             return False
-        x = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(self.session.get("url")))
+        x = xmlrpc.client.ServerProxy( join_url(self.session.get("url"), 'xmlrpc/2/object'))
         return x.execute_kw(
-            self.session.get("database"), 
-            self.session.get("uid"), 
-            self.session.get("password"), 
+            self.session.get("database"),
+            self.session.get("uid"),
+            self.session.get("password"),
             model, method, domain, options)
-    
+
     def cli(self, **kwargs):
         # Overwrite
-        params = self.gpt.gptparse_params()
-        
-        def findbyid(rows, id):
-            for row in rows:
-                for k in row.keys():
-                    if k == 'id' and row.get(k) == id:
-                        return row
-            return None
-
-        if params.odoo_projects:
-            title="Odoo Projects"
+        params = self.gpt.parse.parse_args()
+
+        if hasattr(params, 'odoo_projects') and params.odoo_projects:
             try:
                 rows = self.projects()
                 if params.set:
-                    row = findbyid(rows, int(params.set))
-                    if row: 
-                        self.gpt.gptconfig_set(self.name, "project_id",row.get('id'))
-                        self.gpt.gptconfig_set(self.name, "project_name",row.get('name'))
-                        self.gpt.gptconfig_set(self.name, "task_id","")
-                        self.gpt.gptconfig_set(self.name, "task_name","")
-                        self.gpt.print_cli([], title= 'the project was added successfully')
+                    row = find_by_id(rows, params.set)
+                    if row:
+                        self.gpt.set_config(self.name, "project_id", row.get('id'))
+                        self.gpt.set_config(self.name, "project_name", row.get('name'))
+                        self.gpt.set_config(self.name, "task_id", "")
+                        self.gpt.set_config(self.name, "task_name", "")
+                        printtbl([row])
                     else:
-                        self.gpt.print_cli([], title= 'the project id was not found')
+                        print('The project ID was not found')
                 else:
-                    self.gpt.print_cli(rows, title=title)
+                    printtbl(rows)
             except Exception as e:
-                logger.error(e)
-        elif params.odoo_tasks:
-            title="Odoo Tasks"
+                self.gpt.logger.exception(e)
+        elif hasattr(params, 'odoo_tasks') and params.odoo_tasks:
             try:
                 rows = self.tasks()
                 if params.set:
-                    row = findbyid(rows, int(params.set))
-                    if row: 
-                        self.gpt.gptconfig_set(self.name, "task_id",row.get('id'))
-                        self.gpt.gptconfig_set(self.name, "task_name",row.get('name'))
-                        self.gpt.gptconfig_set(self.name, "project_id",row.get('project_id')[0])
-                        self.gpt.gptconfig_set(self.name, "project_name",row.get('project_id')[1])
-                        self.gpt.print_cli([], title= 'the task was added successfully')
+                    row = find_by_id(rows, params.set)
+                    if row:
+                        self.gpt.set_config(self.name, "task_id", row.get('id'))
+                        self.gpt.set_config(self.name, "task_name", row.get('name'))
+                        self.gpt.set_config(self.name, "project_id", row.get('project_id'))
+                        self.gpt.set_config(self.name, "project_name", row.get('project_name'))
+                        printtbl([row])
                     else:
-                        self.gpt.print_cli([], title= 'the task id was not found')
+                        print('The task ID was not found')
                 else:
-                    self.gpt.print_cli(rows, title=title)
+                    printtbl(rows)
             except Exception as e:
-                logger.error(e)
+                self.gpt.logger.exception(e)
+
+    def data_order(self, rows):
+        nrows = []
+        for row in rows:
+            nrows.append(dict(sorted(row.items(), key=lambda c: str(c[1]))))
+        return len(nrows) and nrows or rows
 
-    
     def projects(self):
-        projects = self.models('project.project', 'search_read',[[['active','=',True]]],{'fields': ['id', 'name']})
+        projects = self.models('project.project', 'search_read', [[['active', '=', True]]], {'fields': ['id', 'name']})
+        projects = self.data_order(projects)
+        self.gpt.logger.info(projects)
         return projects
 
     def tasks(self, project_id=None):
         if project_id:
-            domain = [[['active','=', True], ['project_id','=',int(project_id)]]]
-        else: 
-            domain = [[['active','=', True]]]
-        tasks = self.models('project.task', 'search_read',domain,{'fields': ['id', 'name', 'project_id' ]})
+            domain = [[['active', '=', True], ['project_id', '=', int(project_id)]]]
+        else:
+            domain = [[['active', '=', True]]]
+        tasks = self.models('project.task', 'search_read', domain, {'fields': ['id', 'name', 'project_id' ]})
+        tasks = self.data_order(tasks)
+        if len(tasks):
+            ntasks = []
+            for t in tasks:
+                tb = t.pop('project_id')
+                ntasks.append({**t, 'project_id': tb[0],  'project_name': tb[1]})
+            tasks = ntasks
+        self.gpt.logger.info(tasks)
         return tasks
-    
-    
+
     def add_time_entry(self, **kwargs):
         # Overwrite
         description = kwargs.get('name')
@@ -193,38 +182,26 @@ def add_time_entry(self, **kwargs):
         minutes = float(kwargs.get('minutes', 0))
         name = "%s - DTS:%s DTE:%s" % (description, dt_start, dt_end)
         try:
-            project_id = int(self.gpt.gptconfig_get(self.name, "project_id"))
-            if project_id > 0 :
-                task_id = self.gpt.gptconfig_get(self.name, "task_id")                
-                task_id =  int(task_id) if len(task_id) else  False
-                id = self.models('account.analytic.line','create', [{
-                    'date':datetime.now().strftime("%Y-%m-%d"), # Required
-                    'name': name, # Required
-                    'project_id': project_id, # Required 
-                    'task_id': task_id, 
-                    'unit_amount': minutes/60,
-                }])    
-                return id 
+            project_id = int(self.gpt.get_config(self.name, "project_id"))
+            if project_id > 0:
+                task_id = self.gpt.get_config(self.name, "task_id")
+                task_id = int(task_id) if isinstance(task_id, int) else False
+                id = self.models('account.analytic.line', 'create', [{
+                    'date': datetime.now().strftime("%Y-%m-%d"),  # Required
+                    'name': name,  # Required
+                    'project_id': project_id,  # Required
+                    'task_id': task_id,
+                    'unit_amount': minutes / 60 }])
+                self.gpt.logger.info(id)
+                return {'id': id, 'name': name}
             else:
-                raise Exception ("First select the project  --odoo-projects --set ID")
+                raise Exception("First select the project  --odoo-projects --set ID")
         except Exception as e:
-            logger.error(str(e))
-        return False
-        
-    ## Optional params
-    def status(self, **kwargs):
-        # Overwrite
-        items = []
-        def getstate(param):
-            try:
-                id = self.gpt.gptconfig_get(self.name, param+"_id")
-                name =self.gpt.gptconfig_get(self.name, param+"_name")
-                if len(id) and len(name):
-                    items.append({'name': "%s: %s - %s " % (str(param).title(), id, name)})
-            except:
-                pass
-        getstate('task')
-        getstate('project')
-        self.gpt.print_cli(items)
-
+            self.gpt.logger.exception(e)
+        return None
 
+    # Optional params
+    def status(self, **kwargs):
+        attrs = ['project_name', 'task_name']
+        items = config_attrs(self.gpt, self.name, attrs, formatter='status')
+        printtbl(items)
diff --git a/plugins/toggl.py b/plugins/toggl.py
index 938280d..753a972 100644
--- a/plugins/toggl.py
+++ b/plugins/toggl.py
@@ -1,13 +1,9 @@
-from os import name
-import pdb
-import requests
-import json
-from urllib.parse import urljoin
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import configparser
-import logging
 from .gpt_plugin import GPTPlugin
-
-logger = logging.getLogger(__name__)
+from .gpt_utils import printtbl, join_url,\
+    find_by_id, only_columns, config_attrs
 
 class Toggl(GPTPlugin):
 
@@ -15,179 +11,175 @@ class Toggl(GPTPlugin):
     url = "https://api.track.toggl.com/api/v8"
     token = None
 
-    def __init__(self, gpt):    
+    def __init__(self, gpt):
         super().__init__(gpt)
-    
+
     def setup(self):
         try:
-            self.token = self.gpt.gptconfig_get(self.name, "token")
+            self.token = self.gpt.get_config(self.name, "token")
         except configparser.NoSectionError as e:
-            logger.error(str(e))
-            self.gpt.gptconfig_set_section(self.name)
+            self.gpt.logger.error(e)
+            self.gpt.add_section_config(self.name)
             self.add_parse_args(kind="setup-args")
         except configparser.NoOptionError as e:
+            self.gpt.logger.error(e)
             self.add_parse_args(kind="setup-args")
-            params =  self.gpt.gptparse_params()
+            params =  self.gpt.parse.parse_args()
             self.token = params.toggl_token
             try:
                 if self.auth():
-                    self.gpt.gptconfig_set(self.name, "token", self.token)
-                    print("Now you can use the plugin. Try gp-tracking -h %s" % self.name)
-                else: 
+                    self.gpt.set_config(self.name, "token", self.token)
+                    print(f"{self.name} now can do you use.")
+                else:
                     raise Exception("Fail auth")
             except Exception as e:
-                logger.error(str(e))
+                self.gpt.logger.critical(e)
                 exit(0)
 
     def add_parse_args(self, kind):
         if kind == "setup-args":
             self.gpt.parse.add_argument('--toggl-token',
-                action='store', 
-                dest='toggl_token', 
-                help=' e.g 23bc78d4e46edd5479885db4260ecsf3', 
-                required=True
-            )
+                                        action='store',
+                                        dest='toggl_token',
+                                        help=' e.g 23bc78d4e46edd5479885db4260ecsf3',
+                                        required=True)
         else:
-            self.gpt.parse.add_argument('--toggl-workspaces', 
-                action='store_const', 
-                dest='toggl_workspaces', 
-                help='List workspaces',     
-                const=True,                
-            )
-            self.gpt.parse.add_argument('--toggl-projects', 
-                action='store_const', 
-                dest='toggl_projects', 
-                help='List projects',     
-                const=True,
-            )
-
-    http_auth = lambda self: (self.token, "api_token")
+            self.gpt.parse.add_argument('--toggl-workspaces',
+                                        action='store_const',
+                                        dest='toggl_workspaces',
+                                        help='List workspaces',
+                                        const=True)
+            self.gpt.parse.add_argument('--toggl-projects',
+                                        action='store_const',
+                                        dest='toggl_projects',
+                                        help='List projects',
+                                        const=True)
+
+    def http_auth(self):
+        return (self.token, "api_token")
 
     def auth(self):
         try:
-            data =  self.http_call('GET', "%s/me" % self.url, auth=self.http_auth())
-            if data['data']['id']:
-                return True
-        except Exception as e :
-            pass
+            req = self.rget(join_url(self.url, "me" ), auth=self.http_auth())
+            if req.ok:
+                data = req.json()
+                if data['data']['id']:
+                    return True
+            else:
+                raise Exception(req.text)
+        except Exception as e:
+            self.gpt.logger.exception(e)
         return False
-    
+
     def cli(self):
-        params = self.gpt.gptparse_params()
-
-        def findbyid(rows, id):
-            for row in rows:
-                for k in row.keys():
-                    if k == 'id' and str(row.get(k)) == id:
-                        return row
-            return None
-
-        def onlycolumns(rows):
-            l = []
-            for r in rows: 
-                l.append( { 'id': r.get('id'), 'name':  r.get('name')})
-            return l
-
-        if params.toggl_workspaces:
+        params = self.gpt.parse.parse_args()
+
+        if hasattr(params, 'toggl_workspaces') and params.toggl_workspaces:
             try:
                 rows = self.workspaces()
                 if rows:
-                    rows = onlycolumns(rows)
-                    title ="Toggl Workspaces"
+                    rows = only_columns(rows)
                     if params.set:
-                        row = findbyid(rows, params.set)
-                        if row: 
-                            self.gpt.gptconfig_set(self.name, "workspace_id",row.get('id') )
-                            self.gpt.gptconfig_set(self.name, "workspace_name",row.get('name') )
+                        row = find_by_id(rows, params.set)
+                        if row:
+                            self.gpt.set_config(self.name, "workspace_id", row.get('id') )
+                            self.gpt.set_config(self.name, "workspace_name", row.get('name') )
 
-                            self.gpt.gptconfig_set(self.name, "project_id", "")
-                            self.gpt.gptconfig_set(self.name, "project_name","" )
+                            self.gpt.set_config(self.name, "project_id", "")
+                            self.gpt.set_config(self.name, "project_name", "" )
 
-                            self.gpt.print_cli([], title= 'the workspace was added successfully')
+                            printtbl([row])
                         else:
-                            self.gpt.print_cli([], title= 'the workspace id was not found')
+                            print('The workspace ID was not found')
                     else:
-                        self.gpt.print_cli(rows, title=title)
+                        printtbl(rows)
                 else:
                     raise Exception("Fail to get workspaces")
             except Exception as e:
-                self.gpt.exit(e)
-        elif params.toggl_projects:
+                self.gpt.logger.exception(e)
+        elif hasattr(params, 'toggl_projects') and params.toggl_projects:
             try:
-                workspace_id = self.gpt.gptconfig_get(self.name, "workspace_id")
+                workspace_id = self.gpt.get_config(self.name, "workspace_id")
             except Exception as e:
-                #logger.error(e)
                 workspace = self.workspaces(filter='first')
                 workspace_id = workspace.get('id')
             try:
                 rows = self.projects(workspace_id)
                 if rows:
-                    rows = onlycolumns(rows)
-                    title ="Clockify projects"
+                    rows = only_columns(rows)
                     if params.set:
-                        row = findbyid(rows, params.set)
-                        if row: 
-                            self.gpt.gptconfig_set(self.name, "project_id",row.get('id') )
-                            self.gpt.gptconfig_set(self.name, "project_name",row.get('name') )
-                            self.gpt.print_cli([], title= 'the project was added successfully')
+                        row = find_by_id(rows, params.set)
+                        if row:
+                            self.gpt.set_config(self.name, "project_id", row.get('id') )
+                            self.gpt.set_config(self.name, "project_name", row.get('name') )
+                            printtbl([row])
                         else:
-                            self.gpt.print_cli([], title= 'the project id was not found')
-                    else: 
-                        self.gpt.print_cli(rows, title=title)
+                            print('The project ID was not found')
+                    else:
+                        printtbl(rows)
                 else:
                     raise Exception("Fail to get projects")
             except Exception as e:
-                logger.error(e)
-   
+                self.gpt.logger.exception(e)
+
     def workspaces(self, filter=""):
-        url = self.url +  "/workspaces"
+        url = join_url(self.url, "workspaces")
         try:
-            data = self.http_call('GET', url, auth=self.http_auth())
-            if filter =='first':            
-                return len(data) and data[0]
-            return data
-        except:
-            pass
+            req = self.rget(url, auth=self.http_auth())
+            if req.ok:
+                data = req.json()
+                self.gpt.logger.info(data)
+                if filter == 'first':
+                    return len(data) and data[0]
+                return data
+            else:
+                raise Exception(req.text)
+        except Exception as e:
+            self.gpt.logger.exception(e)
         return None
 
     def projects(self, workspace_id, filter=""):
         try:
-            url = "{}/workspaces/{}/projects".format(self.url,workspace_id)
-            data = self.http_call('GET',url, auth=self.http_auth())
-            if filter =='first' :
+            url = join_url(self.url, "workspaces/{}/projects".format(workspace_id))
+            req = self.rget(url, auth=self.http_auth())
+            if req.ok:
+                data = req.json()
+                self.gpt.logger.info(data)
+                if filter == 'first':
                     return len(data) and data[0]
+            else:
+                raise Exception(req.text)
             return data
-        except :
-            pass
+        except Exception as e:
+            self.gpt.logger.exception(e)
         return None
-    
-    def add_time_entry(self, **kwargs):        
-        description = kwargs.get('name')
-        start= kwargs.get('start')
-        end= kwargs.get('end')
+
+    def add_time_entry(self, **kwargs):
+        name = kwargs.get('name')
+        start = kwargs.get('start')
+        end = kwargs.get('end')
         minutes = kwargs.get('minutes')
-        
+
         workspace_id  = None
         try:
-            workspace_id = self.gpt.gptconfig_get(self.name, "workspace_id")
-        except:
+            workspace_id = self.gpt.get_config(self.name, "workspace_id")
+        except Exception as e:
             try:
                 workspace, err = self.workspaces(filter='first')
                 workspace_id = workspace.get('id')
-            except:
+            except Exception as e:
                 pass
-        
         project_id = None
         try:
-            project_id = self.gpt.gptconfig_get(self.name, "project_id")
-        except:
+            project_id = self.gpt.get_config(self.name, "project_id")
+        except Exception as e:
             pass
-        
+
         time_entry = {
-            "start": start, # Required 
-            "description": description,
+            "start": start,  # Required
+            "description": name,
             "projectId": project_id,
-            "stop": end, # Required
+            "stop": end,  # Required
             "duration": float(minutes) * 60,
             "created_with": "gp-tracking"
         }
@@ -197,33 +189,24 @@ def add_time_entry(self, **kwargs):
 
         if project_id:
             time_entry.update({'pid': project_id})
-        
+
         try:
-            url = self.url + "/time_entries"
-            data = self.http_call(
-                'POST',url, auth= self.http_auth(),
-                json= {"time_entry": time_entry}
+            url = join_url(self.url, "time_entries")
+            req = self.rpost(
+                url, auth=self.http_auth(),
+                json={"time_entry": time_entry}
             )
-            return data["data"]["id"]
+            if req.ok:
+                data = req.json()
+                self.gpt.logger.info(data)
+                return {'id': data['data']['id'], 'name': name}
+            else:
+                raise Exception(req.text)
         except Exception as e:
-            logger.error(e)
-        return -1
-    
-    def status(self):
-        items = []
-        def getstate(param):
-            try:
-                id = self.gpt.gptconfig_get(self.name, param+"_id")
-                name =self.gpt.gptconfig_get(self.name, param+"_name")
-                if len(id) and len(name):
-                    items.append({'name': "%s: %s - %s " % (str(param).title(), id, name)})
-            except:
-                pass
-        getstate('workspace')
-        getstate('project')
-        self.gpt.print_cli(items) 
-    
-
-
-
+            self.gpt.logger.exception(e)
+        return None
 
+    def status(self):
+        attrs = ['workspace_name', 'project_name']
+        items = config_attrs(self.gpt, self.name, attrs, formatter='status')
+        printtbl(items)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 44e96fb..881424c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-pytest==6.1.2
+mock==4.0.3
 requests==2.24.0
 flake8
\ No newline at end of file
diff --git a/semv.toml b/semv.toml
deleted file mode 100644
index 9c99c4f..0000000
--- a/semv.toml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-[version]
-  label = ""
-  major = 3
-  minor = 0
-  patch = 1
diff --git a/startup.sh b/startup.sh
index 2c5cf55..6992187 100755
--- a/startup.sh
+++ b/startup.sh
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 GPT_DIR=${GPT_DIR:-$HOME/.gnome-pomodoro-tracking}
-REMOTE=${REMOTE:-https://github.com/josehbez/gnome-pomodoro-tracking.git}
+REMOTE=${REMOTE:-https://github.com/gnome-pomodoro/gnome-pomodoro-tracking.git}
 GPT_CONF="$HOME/.gnome-pomodoro-tracking.conf"
 GPT_BIN="$HOME/.local/bin/gnome-pomodoro-tracking"
 


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index bbbc9d3..3adbf18 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -1,8 +1,9 @@
 name: Pytest
 
 on:
-  #push:
-  #  branches: [ master ]
+  push:
+    branches: [ master ]
+
   pull_request:
     branches: [ master ]
 
@@ -24,20 +25,10 @@ jobs:
       run: |
         python -m pip install --upgrade pip
         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
-    #- name: Lint with flake8
-    #  run: |
-    #    # stop the build if there are Python syntax errors or undefined names
-    #    flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
-    #    # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
-    #    flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
-    - name: Test with pytest
-      env:
-        GP_TRACKING_ENV: ${{ secrets.GP_TRACKING_ENV }}
-        GP_TRACKING_CLOCKIFY_TOKEN: ${{ secrets.GP_TRACKING_CLOCKIFY_TOKEN }}
-        GP_TRACKING_TOGGL_TOKEN: ${{ secrets.GP_TRACKING_TOGGL_TOKEN }}
-        GP_TRACKING_ODOO_URL: ${{ secrets.GP_TRACKING_ODOO_URL }}
-        GP_TRACKING_ODOO_PASSWORD: ${{ secrets.GP_TRACKING_ODOO_PASSWORD }}
-        GP_TRACKING_ODOO_USERNAME: ${{ secrets.GP_TRACKING_ODOO_USERNAME }}
-        GP_TRACKING_ODOO_DATABASE: ${{ secrets.GP_TRACKING_ODOO_DATABASE }}
+    - name: Lint with flake8
+      run: |
+        flake8 . --count --show-source --statistics
+        flake8 . --count --exit-zero --statistics
+    - name: Test with unittest
       run: |
-        pytest
+        python -m unittest
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index e77aa58..0000000
--- a/tests/README.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Tests
-```bash
-export GP_TRACKING_ENV=Test
-
-# Params clockify
-export GP_TRACKING_CLOCKIFY_TOKEN=Token
-# Params toggl
-export GP_TRACKING_TOGGL_TOKEN=Token
-# Params odoo
-export GP_TRACKING_ODOO_URL=Url
-export GP_TRACKING_ODOO_PASSWORD=Password
-export GP_TRACKING_ODOO_USERNAME=User
-export GP_TRACKING_ODOO_DATABASE=Db
-
-# run tests
-pytest
-```
\ No newline at end of file
diff --git a/tests/test_clockify.py b/tests/test_clockify.py
index 9d1c30a..64f007e 100644
--- a/tests/test_clockify.py
+++ b/tests/test_clockify.py
@@ -1,38 +1,50 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import os
-import pdb
-from .test_gpt_plugin import TestGPTPlugin
-
+from tests.test_gpt_plugin import TestGPTPlugin
+from mock import patch
 
 class TestClockify(TestGPTPlugin):
 
     plugin = "clockify"
+    token = os.getenv("GP_TRACKING_CLOCKIFY_TOKEN", "X/oWnmt2eyj4ZCbh")
 
-    def test_load_plugin(self):        
-        gpt = self.gpt()
-        gpt.gptconfig_settings("plugin", self.plugin)
-        token = os.getenv("GP_TRACKING_CLOCKIFY_TOKEN", "")
-        gpt.gptconfig_set(self.plugin, "token", token)
-        assert gpt.load_plugin() == True, "The plugin do not exists!"
+    workspaces = [
+        {'id': '5e9ca62da2686b699ed5748d', 'name': "Workspace C1"},
+        {'id': '5e9ca62da2786b699ed5748d', 'name': "Workspace C2"}]
+    projects = [
+        {'id': '5eab188c991f8972bb9a1fa3', 'name': 'Project C1', 'workspaceId': '5e9ca62da2686b699ed5748d'},
+        {'id': '5eab188c991f8972bb9a1fa4', 'name': 'Project C2', 'workspaceId': '5e9ca62da2786b699ed5748d'}]
 
-    def test_command_clockify_workspaces(self):
-        args = {'clockify_workspaces': True}
-        self.cli_params_list( args)
+    auth = True
+    time_entry = {'id': '6065003e9341062dc3acf936', 'name': 'Time entry'}
 
-        id, idErr  = self.get_id_stdout()
-        args.update({"set": id})
-        self.cli_params_set(args)
+    def setUp(self) -> None:
+        super(TestClockify, self).setUp()
+        self.gpt.settings_config("plugin", self.plugin)
+        self.gpt.set_config(self.plugin, "token", self.token)
 
-    def test_command_clockify_projects(self):
-        args = {'clockify_projects': True}
-        self.cli_params_list( args)
+        if self.token == 'X/oWnmt2eyj4ZCbh':
+            patch("plugins.clockify.Clockify.auth", return_value=self.auth).start()
+            patch("plugins.clockify.Clockify.workspaces", return_value=self.workspaces).start()
+            patch("plugins.clockify.Clockify.projects", return_value=self.projects).start()
+            patch("plugins.clockify.Clockify.add_time_entry", return_value=self.time_entry).start()
 
-        id, idErr  = self.get_id_stdout()
-        args.update({"set": id})
-        self.cli_params_set(args)
+        self.load_plugin()
 
+    def test_cli(self):
 
-    def test_time_entry(self): 
-        gpt = self.load_plugin()
-        parse_defaults =  {"test_time_entry": True}
-        gpt.parse.set_defaults(**parse_defaults)
-        gpt.cli()
\ No newline at end of file
+        args = {'clockify_workspaces': True}
+        idA = self.cli_list( args)
+        args.update({"set": idA})
+        idB = self.cli_set(args)
+        assert idA == idB
+
+        args.update({'clockify_projects': True, 'clockify_workspaces': False, 'set': False})
+        idA = self.cli_list( args)
+        args.update({"set": idA})
+        idB = self.cli_set(args)
+        assert idA == idB
+
+        args.update({'time_entry': True, 'set': False, 'clockify_projects': False})
+        self.cli_time_entry(args)
diff --git a/tests/test_gpt_plugin.py b/tests/test_gpt_plugin.py
index e93bfba..284dd95 100644
--- a/tests/test_gpt_plugin.py
+++ b/tests/test_gpt_plugin.py
@@ -1,57 +1,41 @@
-from os import supports_effective_ids
-import pdb
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
+import unittest
 import gp_tracking as gpt
-import os
-import re
 
-class TestGPTPlugin(object):
+class TestGPTPlugin(unittest.TestCase):
 
-    def gpt(self):
-        #DIRPATH = gpt.GPTracking.dirpath()
-        DIRHOME = os.path.expanduser("~")
-        return gpt.GPTracking("./gp-tracking.template", "Remove", DIRHOME)
+    def setUp(self) -> None:
+        self.gpt = gpt.GPTracking("gnome-pomodoro-tracking.template")
+        self.gpt.parse.set_defaults(debug=True)
 
     def load_plugin(self):
-        gpt = self.gpt()
-        gpt.load_plugin()
-        gpt.add_parse_args()
-        return gpt
+        self.gpt.load_plugin()
+        self.gpt.add_parse_args()
 
-    def cli_params_list(self, parse_defaults):
-        gpt = self.load_plugin()
-        gpt.parse.set_defaults(**parse_defaults)
-        gpt.cli()
-        id, idErr  = self.get_id_stdout()
-        assert idErr == None, idErr
-    
-    def cli_params_set(self,parse_defaults):
-        gpt = self.load_plugin()
-        gpt.parse.set_defaults(**parse_defaults)
-        gpt.cli()
-        success, successErr = self.set_success_stdout()
-        assert successErr == None, successErr
+    def cli_list(self, parse_defaults):
+        self.gpt.parse.set_defaults(**parse_defaults)
+        self.gpt.cli()
+        id = self.get_stdout_id()
+        return id
 
+    def cli_set(self, parse_defaults):
+        self.gpt.parse.set_defaults(**parse_defaults)
+        self.gpt.cli()
+        id = self.get_stdout_id()
+        return id
 
-    def get_id_stdout(self):
-        data  , err = None, None
-        with open("tests/stdout.txt", 'r') as f:
+    def get_stdout_id(self):
+        id = None
+        with open(".gnome-pomodoro-tracking.log", 'r') as f:
             lines = f.readlines()
-            line_split = str(lines[1]).split("|")
-            if len(line_split)>1:
-                data  = line_split[1].strip()
-            else:
-                err = "Not found Id"
+            line_split = str(lines[len(lines) - 1]).split("gp_tracking.py:write")
+            if len(line_split) == 2:
+                id  = line_split[1].strip().split(' ')[0] or None
             f.close()
-        return data, err 
+        assert isinstance(id, str)
+        return id
 
-    def set_success_stdout(self):
-        data, err= False, None
-        with open("tests/stdout.txt", 'r') as f:
-            lines = f.readlines()
-            line = str(lines[0])
-            if re.match(r'^.*succes.*$', line):
-                data  = True
-            else: 
-                err = line
-            f.close()
-        return data, err
\ No newline at end of file
+    def cli_time_entry(self, parse_defaults):
+        self.gpt.parse.set_defaults(**parse_defaults)
+        self.gpt.cli()
diff --git a/tests/test_odoo.py b/tests/test_odoo.py
index 79c9b45..8cbdf7f 100644
--- a/tests/test_odoo.py
+++ b/tests/test_odoo.py
@@ -1,40 +1,56 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import os
-import pdb
 from .test_gpt_plugin import TestGPTPlugin
-
+from mock import patch
 
 class TestOdoo(TestGPTPlugin):
 
     plugin = "odoo"
+    url = os.getenv("GP_TRACKING_ODOO_URL", "http://local.host")
+    database = os.getenv("GP_TRACKING_ODOO_DATABASE", "localhost")
+    username = os.getenv("GP_TRACKING_ODOO_USERNAME", "username@local.host")
+    password = os.getenv("GP_TRACKING_ODOO_PASSWORD", "password@local.host")
+
+    projects = [
+        {'id': 1, 'name': 'Project O1'},
+        {'id': 2, 'name': 'Project O2'}]
+    tasks = [
+        {'id': 13, 'name': 'Task O1', 'project_id': 1, 'project_name': 'Project O1'},
+        {'id': 14, 'name': 'Task O2', 'project_id': 2, 'project_name': 'Project O1'}]
+
+    auth = True
+    time_entry = {'id': 4, 'name': 'Time entry'}
+
+    def setUp(self) -> None:
+        super(TestOdoo, self).setUp()
+        self.gpt.settings_config("plugin", self.plugin)
+        self.gpt.set_config(self.plugin, "url", self.url)
+        self.gpt.set_config(self.plugin, "database", self.database)
+        self.gpt.set_config(self.plugin, "username", self.username)
+        self.gpt.set_config(self.plugin, "password", self.password)
+
+        if self.url == 'http://local.host':
+            patch("plugins.odoo.Odoo.auth", return_value=self.auth).start()
+            patch("plugins.odoo.Odoo.projects", return_value=self.projects).start()
+            patch("plugins.odoo.Odoo.tasks", return_value=self.tasks).start()
+            patch("plugins.odoo.Odoo.add_time_entry", return_value=self.time_entry).start()
+
+        self.load_plugin()
+
+    def test_cli(self):
 
-    def test_load_plugin(self):        
-        gpt = self.gpt()
-        gpt.gptconfig_settings("plugin", self.plugin)
-        gpt.gptconfig_set(self.plugin, "url",os.getenv("GP_TRACKING_ODOO_URL", ""))
-        gpt.gptconfig_set(self.plugin, "database",os.getenv("GP_TRACKING_ODOO_DATABASE", ""))
-        gpt.gptconfig_set(self.plugin, "username",os.getenv("GP_TRACKING_ODOO_USERNAME", ""))
-        gpt.gptconfig_set(self.plugin, "password",os.getenv("GP_TRACKING_ODOO_PASSWORD", ""))
-        assert gpt.load_plugin() == True, "The plugin do not exists!"
-
-    def test_command_odoo_projects(self):
         args = {'odoo_projects': True}
-        self.cli_params_list( args)
-
-        id, idErr  = self.get_id_stdout()
-        args.update({"set": id})
-        self.cli_params_set(args)
-
-    def test_command_odoo_tasks(self):
-        args = {'odoo_tasks': True}
-        self.cli_params_list( args)
-
-        id, idErr  = self.get_id_stdout()
-        args.update({"set": id})
-        self.cli_params_set(args)
-
-
-    def test_time_entry(self): 
-        gpt = self.load_plugin()
-        parse_defaults =  {"test_time_entry": True}
-        gpt.parse.set_defaults(**parse_defaults)
-        gpt.cli()
\ No newline at end of file
+        idA = self.cli_list(args)
+        args.update({"set": idA})
+        idB = self.cli_set(args)
+        assert idA == idB
+
+        args.update({'odoo_tasks': True, 'odoo_projects': False, 'set': False})
+        idA = self.cli_list(args)
+        args.update({"set": idA})
+        idB = self.cli_set(args)
+        assert idA == idB
+
+        args.update({'time_entry': True, 'set': False, 'odoo_tasks': False})
+        self.cli_time_entry(args)
diff --git a/tests/test_toggl.py b/tests/test_toggl.py
index 4bc8a46..30aa0db 100644
--- a/tests/test_toggl.py
+++ b/tests/test_toggl.py
@@ -1,38 +1,50 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021 The Project GNOME Pomodoro Tracking Authors
 import os
-import pdb
-from .test_gpt_plugin import TestGPTPlugin
-
+from tests.test_gpt_plugin import TestGPTPlugin
+from mock import patch
 
 class TestToggl(TestGPTPlugin):
 
     plugin = "toggl"
+    token = os.getenv("GP_TRACKING_TOGGL_TOKEN", "19c98455494ab3f5d72d91de5c26b116")
+
+    workspaces = [
+        {'id': 4755497, 'name': "Workspace T1"},
+        {'id': 4755487, 'name': "Workspace T2"}]
+    projects = [
+        {'id': 164238639, 'name': 'Project T1', "wid": 4755497},
+        {'id': 168573879, 'name': 'Project T2', 'wid': 4755487}]
+
+    auth = True
+    time_entry = {'id': 1950743713, 'name': 'Time entry'}
+
+    def setUp(self) -> None:
+        super(TestToggl, self).setUp()
+        self.gpt.settings_config("plugin", self.plugin)
+        self.gpt.set_config(self.plugin, "token", self.token)
+
+        if self.token == '19c98455494ab3f5d72d91de5c26b116':
+            patch("plugins.toggl.Toggl.auth", return_value=self.auth).start()
+            patch("plugins.toggl.Toggl.workspaces", return_value=self.workspaces).start()
+            patch("plugins.toggl.Toggl.projects", return_value=self.projects).start()
+            patch("plugins.toggl.Toggl.add_time_entry", return_value=self.time_entry).start()
+
+        self.load_plugin()
 
-    def test_load_plugin(self):        
-        gpt = self.gpt()
-        gpt.gptconfig_settings("plugin", self.plugin)
-        token = os.getenv("GP_TRACKING_TOGGL_TOKEN", "")
-        gpt.gptconfig_set(self.plugin, "token", token)
-        assert gpt.load_plugin() == True, "The plugin do not exists!"
+    def test_cli(self):
 
-    def test_command_toggl_workspaces(self):
         args = {'toggl_workspaces': True}
-        self.cli_params_list( args)
-    
-        id, idErr  = self.get_id_stdout()
-        args.update({"set": id})
-        self.cli_params_set(args)
-
-    def test_command_toggl_projects(self):
-       args = {'toggl_projects': True}
-       self.cli_params_list( args)
-
-       id, idErr  = self.get_id_stdout()
-       args.update({"set": id})
-       self.cli_params_set(args)
-
-
-    def test_time_entry(self): 
-        gpt = self.load_plugin()
-        parse_defaults =  {"test_time_entry": True}
-        gpt.parse.set_defaults(**parse_defaults)
-        gpt.cli()
\ No newline at end of file
+        idA = self.cli_list(args)
+        args.update({"set": idA})
+        idB = self.cli_set(args)
+        assert idA == idB
+
+        args.update({'toggl_projects': True, 'toggl_workspaces': False, 'set': False})
+        idA = self.cli_list(args)
+        args.update({"set": idA})
+        idB = self.cli_set(args)
+        assert idA == idB
+
+        args.update({'time_entry': True, 'set': False, 'toggl_projects': True})
+        self.cli_time_entry(args)