Repository: moira-alert/python-moira-client

Issue:
Add "/setMaintenance" methods

Discussion:

Patch:
diff --git a/moira_client/client.py b/moira_client/client.py
index cf281cf..d171cda 100644
--- a/moira_client/client.py
+++ b/moira_client/client.py
@@ -37,6 +37,9 @@ class Client:
         else:
             self.api_url = api_url
 
+        if not login and auth_user:
+            login = auth_user
+
         self.auth = None
         self.headers = {
             'X-Webauth-User': login,
@@ -101,6 +104,23 @@ class Client:
         except ValueError:
             raise InvalidJSONError(r.content)
 
+    def post(self, path='', **kwargs):
+        """
+
+        :param path: str api path
+        :param kwargs: additional parameters for request
+        :return: dict response
+
+        :raises: HTTPError
+        :raises: InvalidJSONError
+        """
+        r = requests.post(self._path_join(path), headers=self.headers, auth=self.auth, **kwargs)
+        r.raise_for_status()
+        try:
+            return r.json()
+        except ValueError:
+            raise InvalidJSONError(r.content)
+
     def _path_join(self, *args):
         path = self.api_url
         for part in args:
diff --git a/moira_client/models/common.py b/moira_client/models/common.py
new file mode 100644
index 0000000..64e39da
--- /dev/null
+++ b/moira_client/models/common.py
@@ -0,0 +1,18 @@
+DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+
+MINUTES_IN_HOUR = 60
+
+
+def get_schedule(start_hour, start_minute, end_hour, end_minute, disabled_days):
+    days = []
+    for day in DAYS_OF_WEEK:
+        day_info = {
+            'enabled': True if day not in disabled_days else False,
+            'name': day
+        }
+        days.append(day_info)
+    return {
+        'days': days,
+        'startOffset': start_hour * MINUTES_IN_HOUR + start_minute,
+        'endOffset': end_hour * MINUTES_IN_HOUR + end_minute,
+    }
diff --git a/moira_client/models/config.py b/moira_client/models/config.py
new file mode 100644
index 0000000..044e683
--- /dev/null
+++ b/moira_client/models/config.py
@@ -0,0 +1,60 @@
+from ..client import ResponseStructureError
+
+
+class WebContact:
+    def __init__(self, _type, label, validation=None, placeholder=None, _help=None):
+        self.type = _type
+        self.label = label
+        self.validation = validation
+        self.placeholder = placeholder
+        self.help = _help
+
+
+class Config:
+    def __init__(self, remote_allowed, contacts, support_email=None):
+        self.remoteAllowed = remote_allowed
+        self.contacts = contacts
+        self.supportEmail = support_email
+
+
+class ConfigManager:
+    def __init__(self, client):
+        self._client = client
+
+    def fetch(self):
+        """
+        Returns config, see https://moira.readthedocs.io/en/latest/installation/configuration.html
+
+        :return: config
+
+        :raises: ResponseStructureError
+        """
+        result = self._client.get(self._full_path())
+
+        if 'contacts' not in result:
+            raise ResponseStructureError("'contacts' field doesn't exist in response", result)
+        if 'remoteAllowed' not in result:
+            raise ResponseStructureError("'remoteAllowed' field doesn't exist in response", result)
+
+        contacts = []
+        for contact in result['contacts']:
+            if self._validate_contact(contact):
+                _help = contact['help'] if 'help' in contact else None
+                validation = contact['validation'] if 'validation' in contact else None
+                placeholder = contact['placeholder'] if 'placeholder' in contact else None
+                contacts.append(WebContact(_type=contact['type'], label=contact['label'], validation=validation,
+                                           placeholder=placeholder, _help=_help))
+        support = result['supportEmail'] if 'supportEmail' in result else None
+        return Config(remote_allowed=result['remoteAllowed'], contacts=contacts, support_email=support)
+
+    def _validate_contact(self, contact):
+        if 'type' not in contact:
+            return False
+        if 'label' not in contact:
+            return False
+        return True
+
+    def _full_path(self, path=''):
+        if path:
+            return 'config/{}'.format(path)
+        return 'config'
diff --git a/moira_client/models/contact.py b/moira_client/models/contact.py
index a59342f..6f38407 100644
--- a/moira_client/models/contact.py
+++ b/moira_client/models/contact.py
@@ -124,7 +124,42 @@ class ContactManager:
             else:
                 return False
 
+    def update(self, contact):
+        """
+        Updates an existing notification contact
+
+        :param contact: Contact
+        :return: True if ok, False otherwise
+        """
+        data = {
+            'type': contact.type,
+            'value': contact.value,
+        }
+
+        try:
+            self._client.put(self._full_path(contact.id), json=data)
+            return True
+        except InvalidJSONError:
+            return False
+
+    def test(self, contact_id):
+        """
+        Push a test notification to verify that the contact is properly set up.
+
+        :param contact_id: str contact id
+        :return: True if ok, False otherwise
+        """
+
+        try:
+            self._client.post(self._full_path('{id}/test'.format(id=contact_id)))
+            return False
+        except InvalidJSONError as e:
+            if len(e.content) == 0:  # successfully if response is blank
+                return True
+            else:
+                return False
+
     def _full_path(self, path=''):
         if path:
-            return 'contact/' + path
+            return 'contact/{}'.format(path)
         return 'contact'
diff --git a/moira_client/models/event.py b/moira_client/models/event.py
index c1b9400..7c6492f 100644
--- a/moira_client/models/event.py
+++ b/moira_client/models/event.py
@@ -38,7 +38,7 @@ class EventManager:
         :return: True on success, False otherwise
         """
         try:
-            result = self._client.delete(self._full_path("/all"))
+            result = self._client.delete(self._full_path("all"))
             return False
         except InvalidJSONError as e:
             if e.content == b'':  # successfully if response is blank
@@ -47,5 +47,5 @@ class EventManager:
 
     def _full_path(self, path=''):
         if path:
-            return 'event/' + path
+            return 'event/{}'.format(path)
         return 'event'
diff --git a/moira_client/models/health.py b/moira_client/models/health.py
index 5478a3f..61bf691 100644
--- a/moira_client/models/health.py
+++ b/moira_client/models/health.py
@@ -58,5 +58,5 @@ class HealthManager:
 
     def _full_path(self, path=''):
         if path:
-            return 'health/' + path
+            return 'health/{}'.format(path)
         return 'health'
diff --git a/moira_client/models/notification.py b/moira_client/models/notification.py
index 0f2bd74..e9113ba 100644
--- a/moira_client/models/notification.py
+++ b/moira_client/models/notification.py
@@ -11,11 +11,24 @@ class NotificationManager:
         Returns all notifications
         :return: list of dict
 
+        :raises: ResponseStructureError
+        """
+        result = self.fetch(start=0, end=-1)
+        return result
+
+    def fetch(self, start, end):
+        """
+        Gets a paginated list of notifications
+        :return: list of dict
+
+        :param start
+        :param end
+
         :raises: ResponseStructureError
         """
         params = {
-            'start': 0,
-            'end': -1
+            'start': start,
+            'end': end
         }
         result = self._client.get(self._full_path(), params=params)
         if 'list' not in result:
@@ -37,7 +50,26 @@ class NotificationManager:
                 return True
             return False
 
+    def delete(self, notification_id):
+        """
+        Remove notification by id
+
+        :param notification_id: str notification id
+
+        :return: True on success, False otherwise
+        """
+
+        params = {
+            'id': notification_id,
+        }
+        try:
+            result = self._client.delete(self._full_path(), params=params)
+            if 'result' in result and result['result'] == 0:
+                return True
+        except InvalidJSONError as e:
+            return False
+
     def _full_path(self, path=''):
         if path:
-            return 'notification/' + path
+            return 'notification/{}'.format(path)
         return 'notification'
diff --git a/moira_client/models/pattern.py b/moira_client/models/pattern.py
index 0d5d3f6..8174ec2 100644
--- a/moira_client/models/pattern.py
+++ b/moira_client/models/pattern.py
@@ -52,5 +52,5 @@ class PatternManager:
 
     def _full_path(self, path=''):
         if path:
-            return 'pattern/' + path
+            return 'pattern/{}'.format(path)
         return 'pattern'
diff --git a/moira_client/models/subscription.py b/moira_client/models/subscription.py
index 5a9f236..7d89278 100644
--- a/moira_client/models/subscription.py
+++ b/moira_client/models/subscription.py
@@ -1,10 +1,7 @@
 from ..client import InvalidJSONError
 from ..client import ResponseStructureError
 from .base import Base
-
-DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-
-MINUTES_IN_HOUR = 60
+from .common import MINUTES_IN_HOUR, get_schedule
 
 
 class Subscription(Base):
@@ -65,6 +62,7 @@ class Subscription(Base):
             'contacts': self.contacts,
             'tags': self.tags,
             'enabled': self.enabled,
+            'any_tags': self.any_tags,
             'throttling': self.throttling,
             'sched': self.sched,
             'ignore_warnings': self.ignore_warnings,
@@ -75,19 +73,11 @@ class Subscription(Base):
         if subscription_id:
             data['id'] = subscription_id
 
-        data['sched']['days'] = []
-        for day in DAYS_OF_WEEK:
-            day_info = {
-                'enabled': True if day not in self.disabled_days else False,
-                'name': day
-            }
-            data['sched']['days'].append(day_info)
-
-        data['sched']['startOffset'] = self._start_hour * MINUTES_IN_HOUR + self._start_minute
-        data['sched']['endOffset'] = self._end_hour * MINUTES_IN_HOUR + self._end_minute
+        data['sched'] = get_schedule(self._start_hour, self._start_minute, self._end_hour, self._end_minute,
+                                     self.disabled_days)
 
         if subscription_id:
-            result = self._client.put('subscription/' + subscription_id, json=data)
+            result = self._client.put('subscription/{id}'.format(id=subscription_id), json=data)
         else:
             result = self._client.put('subscription', json=data)
         if 'id' not in result:
@@ -254,7 +244,7 @@ class SubscriptionManager:
                         equal = False
                         break
                 except Exception:
-                    raise ValueError('Wrong attibute "{}"'.format(attr))
+                    raise ValueError('Wrong attribute "{}"'.format(attr))
             if equal:
                 return True
         return False
@@ -312,7 +302,7 @@ class SubscriptionManager:
         :return: True on success, False otherwise
         """
         try:
-            self._client.put(self._full_path(subscription_id) + "/test")
+            self._client.put(self._full_path('{id}/test'.format(id=subscription_id)))
             return False
         except InvalidJSONError as e:
             if e.content == b'':  # successfully if response is blank
@@ -321,5 +311,5 @@ class SubscriptionManager:
 
     def _full_path(self, path=''):
         if path:
-            return 'subscription/' + path
+            return 'subscription/{}'.format(path)
         return 'subscription'
diff --git a/moira_client/models/tag.py b/moira_client/models/tag.py
index fbe61db..135189f 100644
--- a/moira_client/models/tag.py
+++ b/moira_client/models/tag.py
@@ -133,5 +133,5 @@ class TagManager:
 
     def _full_path(self, path=''):
         if path:
-            return 'tag/' + path
+            return 'tag/{}'.format(path)
         return 'tag'
diff --git a/moira_client/models/trigger.py b/moira_client/models/trigger.py
index fcdd36a..0ea1fbf 100644
--- a/moira_client/models/trigger.py
+++ b/moira_client/models/trigger.py
@@ -1,9 +1,7 @@
 from ..client import ResponseStructureError
 from ..client import InvalidJSONError
 from .base import Base
-
-
-DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+from .common import MINUTES_IN_HOUR, get_schedule
 
 STATE_OK = 'OK'
 STATE_WARN = 'WARN'
@@ -11,8 +9,6 @@ STATE_ERROR = 'ERROR'
 STATE_NODATA = 'NODATA'
 STATE_EXCEPTION = 'EXCEPTION'
 
-MINUTES_IN_HOUR = 60
-
 RISING_TRIGGER = 'rising'
 FALLING_TRIGGER = 'falling'
 EXPRESSION_TRIGGER = 'expression'
@@ -36,6 +32,7 @@ class Trigger(Base):
             trigger_type=None,
             is_remote=False,
             mute_new_metrics=False,
+            alone_metrics=None,
             **kwargs):
         """
 
@@ -53,6 +50,7 @@ class Trigger(Base):
         :param trigger_type: str trigger type
         :param is_remote: bool use remote storage
         :param mute_new_metrics: bool mute new metrics
+        :param alone_metrics: dict with targets of alone metrics
         :param kwargs: additional parameters
         """
         self._client = client
@@ -90,6 +88,7 @@ class Trigger(Base):
 
         self.is_remote = is_remote
         self.mute_new_metrics = mute_new_metrics
+        self.alone_metrics = alone_metrics
 
     def resolve_type(self, trigger_type):
         """
@@ -160,7 +159,8 @@ class Trigger(Base):
             'expression': self.expression,
             'is_remote': self.is_remote,
             'trigger_type': self.trigger_type,
-            'mute_new_metrics': self.mute_new_metrics
+            'mute_new_metrics': self.mute_new_metrics,
+            'alone_metrics': self.alone_metrics,
         }
 
         if trigger_id:
@@ -168,19 +168,11 @@ class Trigger(Base):
             api_response = TriggerManager(
                 self._client).fetch_by_id(trigger_id)
 
-        data['sched']['days'] = []
-        for day in DAYS_OF_WEEK:
-            day_info = {
-                'enabled': True if day not in self.disabled_days else False,
-                'name': day
-            }
-            data['sched']['days'].append(day_info)
-
-        data['sched']['startOffset'] = self._start_hour * MINUTES_IN_HOUR + self._start_minute
-        data['sched']['endOffset'] = self._end_hour * MINUTES_IN_HOUR + self._end_minute
+        data['sched'] = get_schedule(self._start_hour, self._start_minute, self._end_hour, self._end_minute,
+                                     self.disabled_days)
 
         if trigger_id and api_response:
-            res = self._client.put('trigger/' + trigger_id, json=data)
+            res = self._client.put('trigger/{id}'.format(id=trigger_id), json=data)
         else:
             res = self._client.put('trigger', json=data)
         if 'id' not in res:
@@ -259,9 +251,58 @@ class Trigger(Base):
         for trigger in trigger_manager.fetch_all():
             if self.name == trigger.name and \
                 set(self.targets) == set(trigger.targets) and \
-                set(self.tags) == set(trigger.tags):
+                    set(self.tags) == set(trigger.tags):
                 return trigger
 
+    def get_metrics(self, start, end):
+        """
+        Get metrics associated with certain trigger
+
+        :param start: The start period of metrics to get. Example : -1hour
+        :param end: The end period of metrics to get. Example : now
+
+        :return: Metrics for trigger
+        """
+        try:
+            params = {
+                'from': start,
+                'to': end,
+            }
+            result = self._client.get('trigger/{id}/metrics'.format(id=self.id), params=params)
+            return result
+        except InvalidJSONError:
+            return []
+
+    def delete_metric(self, metric_name):
+        """
+        Deletes metric from last check and all trigger pattern metrics
+
+        :param metric_name: str name of the target metric, example: DevOps.my_server.hdd.freespace_mbytes
+
+        :return: True if success, False otherwise
+        """
+        try:
+            params = {
+                'name': metric_name,
+            }
+            self._client.delete('trigger/{id}/metrics'.format(id=self.id), params=params)
+            return True
+        except InvalidJSONError:
+            return False
+
+    def delete_nodata_metrics(self):
+        """
+        Deletes all metrics from last data which are in NODATA state.
+        It also deletes all trigger patterns of those metrics.
+
+        :return: True if success, False otherwise
+        """
+        try:
+            self._client.delete('trigger/{id}/metrics/nodata'.format(id=self.id))
+            return True
+        except InvalidJSONError:
+            return False
+
 
 class TriggerManager:
     def __init__(self, client):
@@ -297,13 +338,36 @@ class TriggerManager:
 
         :raises: ResponseStructureError
         """
-        result = self._client.get(self._full_path(trigger_id + '/state'))
+        result = self._client.get(self._full_path('{id}/state'.format(id=trigger_id)))
         if 'state' in result:
             trigger = self._client.get(self._full_path(trigger_id))
             return Trigger(self._client, **trigger)
         elif not 'trigger_id' in result:
             raise ResponseStructureError("invalid api response", result)
 
+    def search(self, only_problems, page, text):
+        """
+        Search triggers
+
+        :param only_problems: Restricts the result to errors only. Example: false
+        :param page: Defines the number of the displayed page. E.g, page=2 would display the 2nd page. Example: 1
+        :param text: Query to perform a search for. Example: cpu
+
+        :return: matching triggers list
+
+        :raises: ResponseStructureError
+        """
+        params = {
+            'onlyProblems': only_problems,
+            'page': page,
+            'text': text,
+        }
+        result = self._client.get(self._full_path(), params=params)
+        if 'list' not in result:
+            raise ResponseStructureError("list doesn't exist in response", result)
+
+        return result['list']
+
     def delete(self, trigger_id):
         """
         Delete trigger by trigger id
@@ -317,6 +381,21 @@ class TriggerManager:
         except InvalidJSONError:
             return True
 
+    def get_throttling(self, trigger_id):
+        """
+        Get a trigger with its throttling i.e its next allowed message time
+
+        :param trigger_id: str trigger id
+        :return: trigger throttle value or None
+        """
+        try:
+            result = self._client.get(self._full_path('{id}/throttling'.format(id=trigger_id)))
+            if 'throttling' in result:
+                return result['throttling']
+            return None
+        except InvalidJSONError:
+            return None
+
     def reset_throttling(self, trigger_id):
         """
         Resets throttling by trigger id
@@ -325,7 +404,7 @@ class TriggerManager:
         :return: True if reset, False otherwise
         """
         try:
-            self._client.delete(self._full_path(trigger_id + '/throttling'))
+            self._client.delete(self._full_path('{id}/throttling'.format(id=trigger_id)))
             return True
         except InvalidJSONError:
             return False
@@ -337,7 +416,27 @@ class TriggerManager:
         :param trigger_id: str trigger id
         :return: state of trigger
         """
-        return self._client.get(self._full_path(trigger_id + '/state'))
+        return self._client.get(self._full_path('{id}/state'.format(id=trigger_id)))
+
+    def get_metrics(self, trigger_id, _from, to):
+        """
+        Get metrics associated with certain trigger
+
+        :param trigger_id: str trigger id
+        :param _from: The start period of metrics to get. Example : -1hour
+        :param to: The end period of metrics to get. Example : now
+
+        :return: Metrics for trigger
+        """
+        try:
+            params = {
+                'from': _from,
+                'to': to,
+            }
+            result = self._client.get(self._full_path('{id}/metrics'.format(id=trigger_id)), params=params)
+            return result
+        except InvalidJSONError:
+            return []
 
     def remove_metric(self, trigger_id, metric):
         """
@@ -351,11 +450,23 @@ class TriggerManager:
             params = {
                 'name': metric
             }
-            self._client.delete(self._full_path(trigger_id + '/metrics'), params=params)
+            self._client.delete(self._full_path('{id}/metrics'.format(id=trigger_id)), params=params)
             return True
         except InvalidJSONError:
             return False
 
+    def remove_nodata_metrics(self, trigger_id):
+        """
+        Remove metric by trigger id
+
+        :param trigger_id: str trigger id
+        :return: True if removed, False otherwise
+        """
+        try:
+            self._client.delete(self._full_path('{id}/metrics/nodata'.format(id=trigger_id)))
+            return True
+        except InvalidJSONError:
+            return False
 
     def is_exist(self, trigger):
         """
@@ -367,7 +478,7 @@ class TriggerManager:
         for moira_trigger in self.fetch_all():
             if trigger.name == moira_trigger.name and \
                 set(trigger.targets) == set(moira_trigger.targets) and \
-                set(trigger.tags) == set(moira_trigger.tags):
+                    set(trigger.tags) == set(moira_trigger.tags):
                 return True
         return False
 
@@ -393,6 +504,26 @@ class TriggerManager:
 
         return non_existent
 
+    def set_maintenance(self, trigger_id, end_time, metrics=None):
+        """
+        Set maintenance trigger id or metric name
+
+        :param trigger_id: str trigger id
+        :param metrics: "metric name" - "end-time" map, end-time like param end_time
+        :param end_time: unix time to end the scheduled maintenance
+        :return: True if success, False otherwise
+        """
+        try:
+            data = {
+                'trigger': end_time,
+            }
+            if metrics is not None:
+                data['metrics'] = metrics
+            self._client.put(self._full_path('{id}/setMaintenance'.format(id=trigger_id)), json=data)
+            return True
+        except InvalidJSONError:
+            return False
+
     def create(
             self,
             name,
@@ -408,6 +539,7 @@ class TriggerManager:
             trigger_type=None,
             is_remote=False,
             mute_new_metrics=False,
+            alone_metrics=None,
             **kwargs
     ):
         """
@@ -425,6 +557,7 @@ class TriggerManager:
         :param trigger_type: str trigger type
         :param is_remote: bool use remote storage
         :param mute_new_metrics: bool mute new metrics
+        :param alone_metrics: dict with targets of alone metrics
         :param kwargs: additional trigger params
         :return: Trigger
         """
@@ -443,10 +576,11 @@ class TriggerManager:
             trigger_type,
             is_remote,
             mute_new_metrics,
+            alone_metrics,
             **kwargs
         )
 
     def _full_path(self, path=''):
         if path:
-            return 'trigger/' + path
+            return 'trigger/{}'.format(path)
         return 'trigger'
diff --git a/moira_client/models/user.py b/moira_client/models/user.py
new file mode 100644
index 0000000..5b1c2fe
--- /dev/null
+++ b/moira_client/models/user.py
@@ -0,0 +1,59 @@
+from ..client import ResponseStructureError
+from .contact import Contact
+from .subscription import Subscription
+
+
+class UserSettings:
+    def __init__(self, login, contacts, subscriptions):
+        self.login = login
+        self.contacts = contacts
+        self.subscriptions = subscriptions
+
+
+class UserManager:
+    def __init__(self, client):
+        self._client = client
+
+    def get_username(self):
+        """
+        Gets the username of the authenticated user if it is available.
+
+        :return: login
+
+        :raises: ResponseStructureError
+        """
+        result = self._client.get(self._full_path())
+        if 'login' not in result:
+            raise ResponseStructureError("'login' field doesn't exist in response", result)
+        return result['login']
+
+    def get_user_settings(self):
+        """
+        Get the user's contacts and subscriptions.
+
+        :return: user settings
+
+        :raises: ResponseStructureError
+        """
+        result = self._client.get(self._full_path('settings'))
+        required = ['login', 'contacts', 'subscriptions']
+        for field in required:
+            if field not in result:
+                raise ResponseStructureError("'{}' field doesn't exist in response".format(field), result)
+
+        contacts = []
+        for contact in result['contacts']:
+            contacts.append(Contact(**contact))
+        result['contacts'] = contacts
+
+        subscriptions = []
+        for subscription in result['subscriptions']:
+            subscriptions.append(Subscription(self._client, **subscription))
+        result['subscriptions'] = subscriptions
+
+        return UserSettings(**result)
+
+    def _full_path(self, path=''):
+        if path:
+            return 'user/{}'.format(path)
+        return 'user'
diff --git a/moira_client/moira.py b/moira_client/moira.py
index 18a22d0..8c18c9f 100644
--- a/moira_client/moira.py
+++ b/moira_client/moira.py
@@ -7,6 +7,8 @@ from .models.subscription import SubscriptionManager
 from .models.tag import TagManager
 from .models.trigger import TriggerManager
 from .models.health import HealthManager
+from .models.config import ConfigManager
+from .models.user import UserManager
 
 
 class Moira:
@@ -30,6 +32,8 @@ class Moira:
         self._pattern = None
         self._subscription = None
         self._health = None
+        self._config = None
+        self._user = None
 
     @property
     def trigger(self):
@@ -118,3 +122,26 @@ class Moira:
         if not self._health:
             self._health = HealthManager(self._client)
         return self._health
+
+    @property
+    def config(self):
+        """
+        Get config manager
+
+        :return: ConfigManager
+        """
+        if not self._config:
+            self._config = ConfigManager(self._client)
+        return self._config
+
+    @property
+    def user(self):
+        """
+        Get user manager
+
+        :return: UserManager
+
+        """
+        if not self._user:
+            self._user = UserManager(self._client)
+        return self._user


Test patch:
diff --git a/tests/models/test_config.py b/tests/models/test_config.py
new file mode 100644
index 0000000..4edf6b5
--- /dev/null
+++ b/tests/models/test_config.py
@@ -0,0 +1,51 @@
+try:
+    from unittest.mock import Mock
+    from unittest.mock import patch
+except ImportError:
+    from mock import Mock
+    from mock import patch
+
+from moira_client.client import Client
+from moira_client.client import ResponseStructureError
+from .test_model import ModelTest
+from moira_client.models.config import ConfigManager
+
+
+class ConfigTest(ModelTest):
+
+    def test_fetch(self):
+        client = Client(self.api_url)
+        config_manager = ConfigManager(client)
+
+        with patch.object(client, 'get', return_value={'remoteAllowed': True, 'contacts': []}) as get_mock:
+            res = config_manager.fetch()
+
+        self.assertTrue(get_mock.called)
+
+    def test_fetch_with_contacts(self):
+        client = Client(self.api_url)
+        config_manager = ConfigManager(client)
+
+        with patch.object(client, 'get', return_value={'remoteAllowed': True,
+                                                       'contacts': [{'label': 'Telegram', 'type': 'telegram'}]}
+                          ) as get_mock:
+            res = config_manager.fetch()
+        self.assertTrue(res.remoteAllowed)
+
+        self.assertEqual(res.contacts[0].type, 'telegram')
+        self.assertEqual(res.contacts[0].label, 'Telegram')
+        self.assertTrue(get_mock.called)
+
+    def test_fetch_fail_without_required_fields(self):
+        client = Client(self.api_url)
+        config_manager = ConfigManager(client)
+
+        with patch.object(client, 'get', return_value={'remoteAllowed': True}) as get_mock:
+            with self.assertRaises(ResponseStructureError):
+                config_manager.fetch()
+        self.assertTrue(get_mock.called)
+
+        with patch.object(client, 'get', return_value={'contacts': True}) as get_mock:
+            with self.assertRaises(ResponseStructureError):
+                config_manager.fetch()
+        self.assertTrue(get_mock.called)
diff --git a/tests/models/test_subscription.py b/tests/models/test_subscription.py
index 7691b77..07801b0 100644
--- a/tests/models/test_subscription.py
+++ b/tests/models/test_subscription.py
@@ -14,6 +14,32 @@ from .test_model import ModelTest
 
 class SubscriptionTest(ModelTest):
 
+    def test_create(self):
+        client = Client(self.api_url)
+        manager = SubscriptionManager(client)
+
+        tags = ['server', 'cpu']
+        contacts = ['acd2db98-1659-4a2f-b227-52d71f6e3ba1']
+        s = manager.create(tags, contacts)
+
+        with patch.object(client, 'put', return_value={"id": "e5cd5d73-d893-42b5-98b5-f9bd6c7bc501"}) as put_mock:
+            s.save()
+
+        self.assertTrue(put_mock.called)
+        args_ = put_mock.call_args[1]
+        body_json = args_['json']
+        # check required fields
+        self.assertEqual(tags, body_json['tags'])
+        self.assertEqual(contacts, body_json['contacts'])
+        # check default values
+        self.assertEqual(True, body_json['enabled'])
+        self.assertEqual(True, body_json['throttling'])
+        self.assertTrue('sched' in body_json)
+        self.assertEqual(False, body_json['ignore_warnings'])
+        self.assertEqual(False, body_json['ignore_recoverings'])
+        self.assertTrue('plotting' in body_json)
+        self.assertEqual(False, body_json['any_tags'])
+
     def test_fetch_all(self):
         client = Client(self.api_url)
         subscription_manager = SubscriptionManager(client)
diff --git a/tests/models/test_trigger.py b/tests/models/test_trigger.py
index c769bc4..0ecf22e 100644
--- a/tests/models/test_trigger.py
+++ b/tests/models/test_trigger.py
@@ -61,6 +61,45 @@ class TriggerTest(ModelTest):
         self.assertTrue(res)
         delete_mock.assert_called_with('trigger/' + trigger_id)
 
+    def test_setMaintenance(self):
+        client = Client(self.api_url)
+        trigger_manager = TriggerManager(client)
+
+        trigger_id = '1'
+        end_timestamp = 1612260000
+
+        with patch.object(client, 'put') as put_mock:
+            res = trigger_manager.set_maintenance(trigger_id, end_timestamp)
+
+        self.assertTrue(put_mock.called)
+        self.assertTrue(res)
+        expected_request_data = {
+            'trigger': end_timestamp,
+        }
+        put_mock.assert_called_with('trigger/' + trigger_id + '/setMaintenance', json=expected_request_data)
+
+    def test_setMaintenance_with_metrics(self):
+        client = Client(self.api_url)
+        trigger_manager = TriggerManager(client)
+
+        trigger_id = '1'
+        end_timestamp = 1612260000
+        metrics = {
+            'metric': end_timestamp,
+            'metric2': 1612260555,
+        }
+
+        with patch.object(client, 'put') as put_mock:
+            res = trigger_manager.set_maintenance(trigger_id, end_timestamp, metrics)
+
+        self.assertTrue(put_mock.called)
+        self.assertTrue(res)
+        expected_request_data = {
+            'trigger': end_timestamp,
+            'metrics': metrics,
+        }
+        put_mock.assert_called_with('trigger/' + trigger_id + '/setMaintenance', json=expected_request_data)
+
     def test_fetch_by_id(self):
         client = Client(self.api_url)
         trigger_manager = TriggerManager(client)
diff --git a/tests/models/test_user.py b/tests/models/test_user.py
new file mode 100644
index 0000000..7cc89a6
--- /dev/null
+++ b/tests/models/test_user.py
@@ -0,0 +1,83 @@
+try:
+    from unittest.mock import Mock
+    from unittest.mock import patch
+except ImportError:
+    from mock import Mock
+    from mock import patch
+
+from moira_client.client import Client
+from moira_client.models.user import UserManager
+
+from .test_model import ModelTest
+
+
+class UserTest(ModelTest):
+
+    def test_get_settings(self):
+        client = Client(self.api_url)
+        user_manager = UserManager(client)
+
+        with patch.object(client, 'get', return_value={'login': 'aaa', 'contacts': [], 'subscriptions': []}
+                          ) as get_mock:
+            res_settings = user_manager.get_user_settings()
+
+        self.assertEqual(res_settings.login, 'aaa')
+
+        self.assertTrue(get_mock.called)
+
+    def test_response_example_from_docs(self):
+        client = Client(self.api_url)
+        user_manager = UserManager(client)
+
+        response = {
+          "login": "john",
+          "contacts": [
+            {
+              "id": "1dd38765-c5be-418d-81fa-7a5f879c2315",
+              "user": "",
+              "type": "mail",
+              "value": "devops@example.com"
+            }
+          ],
+          "subscriptions": [
+            {
+              "contacts": [
+                "acd2db98-1659-4a2f-b227-52d71f6e3ba1"
+              ],
+              "tags": [
+                "server",
+                "cpu"
+              ],
+              "sched": {
+                "days": [
+                  {
+                    "enabled": True,
+                    "name": "Mon"
+                  }
+                ],
+                "tzOffset": -60,
+                "startOffset": 0,
+                "endOffset": 1439
+              },
+              "plotting": {
+                "enabled": True,
+                "theme": "dark"
+              },
+              "id": "292516ed-4924-4154-a62c-ebe312431fce",
+              "enabled": True,
+              "any_tags": False,
+              "ignore_warnings": False,
+              "ignore_recoverings": False,
+              "throttling": False,
+              "user": ""
+            }
+          ]
+        }
+
+        with patch.object(client, 'get', return_value=response) as get_mock:
+            res_settings = user_manager.get_user_settings()
+
+        self.assertEqual(res_settings.login, 'john')
+        self.assertEqual(res_settings.contacts[0].id, '1dd38765-c5be-418d-81fa-7a5f879c2315')
+
+        self.assertTrue(get_mock.called)
\ No newline at end of file
diff --git a/tests/test_client.py b/tests/test_client.py
index 658e3ab..3396a43 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -62,6 +62,21 @@ class ClientTest(unittest.TestCase):
         expected_url_call = TEST_API_URL + '/' + test_path
         mock_put.assert_called_with(expected_url_call, data=test_data, headers=TEST_HEADERS, auth=None)
 
+    def test_post(self):
+        def post(url, data, **kwargs):
+            pass
+
+        with patch.object(requests, 'post', side_effects=post) as mock_post:
+            test_path = 'test_path'
+            test_data = {'test': 'test'}
+
+            client = Client(TEST_API_URL, TEST_HEADERS)
+            client.post(test_path, data=test_data)
+
+        self.assertTrue(mock_post.called)
+        expected_url_call = TEST_API_URL + '/' + test_path
+        mock_post.assert_called_with(expected_url_call, data=test_data, headers=TEST_HEADERS, auth=None)
+
     def test_delete(self):
 
         def delete(url, **kwargs):