Browse Source

Add support for POST /v2/dataframes API endpoint to the client

Support for the ``/v2/dataframes`` endpoint has been added to the client.
A new ``dataframes add`` CLI command is also available.

Change-Id: I7fe9072d7280f251edc865a653a0b9ed2ab26c90
Story: 2005890
Task: 35970
changes/71/678571/4
Justin Ferrieu 3 weeks ago
parent
commit
c8d7a9e1c5

+ 3
- 0
cloudkittyclient/common/client.py View File

@@ -26,6 +26,9 @@ class BaseClient(object):
26 26
                  insecure=False,
27 27
                  **kwargs):
28 28
         adapter_options.setdefault('service_type', 'rating')
29
+        adapter_options.setdefault('additional_headers', {
30
+            'Content-Type': 'application/json',
31
+        })
29 32
 
30 33
         if insecure:
31 34
             verify_cert = False

+ 14
- 8
cloudkittyclient/tests/functional/base.py View File

@@ -24,24 +24,30 @@ from cloudkittyclient.tests import utils
24 24
 class BaseFunctionalTest(utils.BaseTestCase):
25 25
 
26 26
     def _run(self, executable, action,
27
-             flags='', params='', fmt='-f json', has_output=True):
27
+             flags='', params='', fmt='-f json', stdin=None, has_output=True):
28 28
         if not has_output:
29 29
             fmt = ''
30 30
         cmd = ' '.join([executable, flags, action, params, fmt])
31 31
         cmd = shlex.split(cmd)
32
-        p = subprocess.Popen(cmd, env=os.environ.copy(), shell=False,
33
-                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
34
-        stdout, stderr = p.communicate()
32
+        p = subprocess.Popen(
33
+            cmd, env=os.environ.copy(), shell=False,
34
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
35
+            stdin=subprocess.PIPE if stdin else None,
36
+        )
37
+        stdout, stderr = p.communicate(input=stdin)
35 38
         if p.returncode != 0:
36 39
             raise RuntimeError('"{cmd}" returned {val}: {msg}'.format(
37 40
                 cmd=' '.join(cmd), val=p.returncode, msg=stderr))
38 41
         return json.loads(stdout) if has_output else None
39 42
 
40 43
     def openstack(self, action,
41
-                  flags='', params='', fmt='-f json', has_output=True):
44
+                  flags='', params='', fmt='-f json',
45
+                  stdin=None, has_output=True):
42 46
         return self._run('openstack rating', action,
43
-                         flags, params, fmt, has_output)
47
+                         flags, params, fmt, stdin, has_output)
44 48
 
45 49
     def cloudkitty(self, action,
46
-                   flags='', params='', fmt='-f json', has_output=True):
47
-        return self._run('cloudkitty', action, flags, params, fmt, has_output)
50
+                   flags='', params='', fmt='-f json',
51
+                   stdin=None, has_output=True):
52
+        return self._run('cloudkitty', action, flags, params, fmt,
53
+                         stdin, has_output)

+ 169
- 0
cloudkittyclient/tests/functional/v2/test_dataframes.py View File

@@ -0,0 +1,169 @@
1
+# Copyright 2019 Objectif Libre
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+#
15
+import os
16
+import uuid
17
+
18
+from cloudkittyclient.tests.functional import base
19
+
20
+
21
+class CkDataframesTest(base.BaseFunctionalTest):
22
+    dataframes_data = """
23
+    {
24
+        "dataframes": [
25
+            {
26
+                "period": {
27
+                    "begin": "20190723T122810Z",
28
+                    "end": "20190723T132810Z"
29
+                },
30
+                "usage": {
31
+                    "metric_one": [
32
+                        {
33
+                            "vol": {
34
+                                "unit": "GiB",
35
+                                "qty": 1.2
36
+                            },
37
+                            "rating": {
38
+                                "price": 0.04
39
+                            },
40
+                            "groupby": {
41
+                                "group_one": "one",
42
+                                "group_two": "two"
43
+                            },
44
+                            "metadata": {
45
+                                "attr_one": "one",
46
+                                "attr_two": "two"
47
+                            }
48
+                        }
49
+                    ],
50
+                    "metric_two": [
51
+                        {
52
+                            "vol": {
53
+                                "unit": "MB",
54
+                                "qty": 200.4
55
+                            },
56
+                            "rating": {
57
+                                "price": 0.06
58
+                            },
59
+                            "groupby": {
60
+                                "group_one": "one",
61
+                                "group_two": "two"
62
+                            },
63
+                            "metadata": {
64
+                                "attr_one": "one",
65
+                                "attr_two": "two"
66
+                            }
67
+                        }
68
+                    ]
69
+                }
70
+            },
71
+            {
72
+                "period": {
73
+                    "begin": "20190823T122810Z",
74
+                    "end": "20190823T132810Z"
75
+                },
76
+                "usage": {
77
+                    "metric_one": [
78
+                        {
79
+                            "vol": {
80
+                                "unit": "GiB",
81
+                                "qty": 2.4
82
+                            },
83
+                            "rating": {
84
+                                "price": 0.08
85
+                            },
86
+                            "groupby": {
87
+                                "group_one": "one",
88
+                                "group_two": "two"
89
+                            },
90
+                            "metadata": {
91
+                                "attr_one": "one",
92
+                                "attr_two": "two"
93
+                            }
94
+                        }
95
+                    ],
96
+                    "metric_two": [
97
+                        {
98
+                            "vol": {
99
+                                "unit": "MB",
100
+                                "qty": 400.8
101
+                            },
102
+                            "rating": {
103
+                                "price": 0.12
104
+                            },
105
+                            "groupby": {
106
+                                "group_one": "one",
107
+                                "group_two": "two"
108
+                            },
109
+                            "metadata": {
110
+                                "attr_one": "one",
111
+                                "attr_two": "two"
112
+                            }
113
+                        }
114
+                    ]
115
+                }
116
+            }
117
+        ]
118
+    }
119
+    """
120
+
121
+    def __init__(self, *args, **kwargs):
122
+        super(CkDataframesTest, self).__init__(*args, **kwargs)
123
+        self.runner = self.cloudkitty
124
+
125
+    def setUp(self):
126
+        super(CkDataframesTest, self).setUp()
127
+
128
+        self.fixture_file_name = '{}.json'.format(uuid.uuid4())
129
+        with open(self.fixture_file_name, 'w') as f:
130
+            f.write(self.dataframes_data)
131
+
132
+    def tearDown(self):
133
+        files = os.listdir('.')
134
+        if self.fixture_file_name in files:
135
+            os.remove(self.fixture_file_name)
136
+
137
+        super(CkDataframesTest, self).tearDown()
138
+
139
+    def test_dataframes_add_with_no_args(self):
140
+        self.assertRaisesRegexp(
141
+            RuntimeError,
142
+            'error: too few arguments',
143
+            self.runner,
144
+            'dataframes add',
145
+            fmt='',
146
+            has_output=False,
147
+        )
148
+
149
+    def test_dataframes_add(self):
150
+        self.runner(
151
+            'dataframes add {}'.format(self.fixture_file_name),
152
+            fmt='',
153
+            has_output=False,
154
+        )
155
+
156
+    def test_dataframes_add_with_hyphen_stdin(self):
157
+        with open(self.fixture_file_name, 'r') as f:
158
+            self.runner(
159
+                'dataframes add -',
160
+                fmt='',
161
+                stdin=f.read().encode(),
162
+                has_output=False,
163
+            )
164
+
165
+
166
+class OSCDataframesTest(CkDataframesTest):
167
+    def __init__(self, *args, **kwargs):
168
+        super(OSCDataframesTest, self).__init__(*args, **kwargs)
169
+        self.runner = self.openstack

+ 2
- 0
cloudkittyclient/tests/unit/v2/base.py View File

@@ -13,6 +13,7 @@
13 13
 #    under the License.
14 14
 
15 15
 from cloudkittyclient.tests import utils
16
+from cloudkittyclient.v2 import dataframes
16 17
 from cloudkittyclient.v2 import scope
17 18
 from cloudkittyclient.v2 import summary
18 19
 
@@ -22,5 +23,6 @@ class BaseAPIEndpointTestCase(utils.BaseTestCase):
22 23
     def setUp(self):
23 24
         super(BaseAPIEndpointTestCase, self).setUp()
24 25
         self.api_client = utils.FakeHTTPClient()
26
+        self.dataframes = dataframes.DataframesManager(self.api_client)
25 27
         self.scope = scope.ScopeManager(self.api_client)
26 28
         self.summary = summary.SummaryManager(self.api_client)

+ 151
- 0
cloudkittyclient/tests/unit/v2/test_dataframes.py View File

@@ -0,0 +1,151 @@
1
+# Copyright 2019 Objectif Libre
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+#
15
+import json
16
+
17
+from cloudkittyclient import exc
18
+from cloudkittyclient.tests.unit.v2 import base
19
+
20
+
21
+class TestDataframes(base.BaseAPIEndpointTestCase):
22
+    dataframes_data = """
23
+    {
24
+        "dataframes": [
25
+            {
26
+                "period": {
27
+                    "begin": "20190723T122810Z",
28
+                    "end": "20190723T132810Z"
29
+                },
30
+                "usage": {
31
+                    "metric_one": [
32
+                        {
33
+                            "vol": {
34
+                                "unit": "GiB",
35
+                                "qty": 1.2
36
+                            },
37
+                            "rating": {
38
+                                "price": 0.04
39
+                            },
40
+                            "groupby": {
41
+                                "group_one": "one",
42
+                                "group_two": "two"
43
+                            },
44
+                            "metadata": {
45
+                                "attr_one": "one",
46
+                                "attr_two": "two"
47
+                            }
48
+                        }
49
+                    ],
50
+                    "metric_two": [
51
+                        {
52
+                            "vol": {
53
+                                "unit": "MB",
54
+                                "qty": 200.4
55
+                            },
56
+                            "rating": {
57
+                                "price": 0.06
58
+                            },
59
+                            "groupby": {
60
+                                "group_one": "one",
61
+                                "group_two": "two"
62
+                            },
63
+                            "metadata": {
64
+                                "attr_one": "one",
65
+                                "attr_two": "two"
66
+                            }
67
+                        }
68
+                    ]
69
+                }
70
+            },
71
+            {
72
+                "period": {
73
+                    "begin": "20190823T122810Z",
74
+                    "end": "20190823T132810Z"
75
+                },
76
+                "usage": {
77
+                    "metric_one": [
78
+                        {
79
+                            "vol": {
80
+                                "unit": "GiB",
81
+                                "qty": 2.4
82
+                            },
83
+                            "rating": {
84
+                                "price": 0.08
85
+                            },
86
+                            "groupby": {
87
+                                "group_one": "one",
88
+                                "group_two": "two"
89
+                            },
90
+                            "metadata": {
91
+                                "attr_one": "one",
92
+                                "attr_two": "two"
93
+                            }
94
+                        }
95
+                    ],
96
+                    "metric_two": [
97
+                        {
98
+                            "vol": {
99
+                                "unit": "MB",
100
+                                "qty": 400.8
101
+                            },
102
+                            "rating": {
103
+                                "price": 0.12
104
+                            },
105
+                            "groupby": {
106
+                                "group_one": "one",
107
+                                "group_two": "two"
108
+                            },
109
+                            "metadata": {
110
+                                "attr_one": "one",
111
+                                "attr_two": "two"
112
+                            }
113
+                        }
114
+                    ]
115
+                }
116
+            }
117
+        ]
118
+    }
119
+    """
120
+
121
+    def test_add_dataframes_with_string(self):
122
+        self.dataframes.add_dataframes(
123
+            dataframes=self.dataframes_data,
124
+        )
125
+        self.api_client.post.assert_called_once_with(
126
+            '/v2/dataframes',
127
+            data=self.dataframes_data,
128
+        )
129
+
130
+    def test_add_dataframes_with_json_object(self):
131
+        json_data = json.loads(self.dataframes_data)
132
+
133
+        self.dataframes.add_dataframes(
134
+            dataframes=json_data,
135
+        )
136
+        self.api_client.post.assert_called_once_with(
137
+            '/v2/dataframes',
138
+            data=json.dumps(json_data),
139
+        )
140
+
141
+    def test_add_dataframes_with_neither_string_nor_object_raises_exc(self):
142
+        self.assertRaises(
143
+            exc.InvalidArgumentError,
144
+            self.dataframes.add_dataframes,
145
+            dataframes=[open],
146
+        )
147
+
148
+    def test_add_dataframes_with_no_args_raises_exc(self):
149
+        self.assertRaises(
150
+            exc.ArgumentRequired,
151
+            self.dataframes.add_dataframes)

+ 2
- 0
cloudkittyclient/v2/client.py View File

@@ -14,6 +14,7 @@
14 14
 #    under the License.
15 15
 #
16 16
 from cloudkittyclient.v1 import client
17
+from cloudkittyclient.v2 import dataframes
17 18
 from cloudkittyclient.v2 import scope
18 19
 from cloudkittyclient.v2 import summary
19 20
 
@@ -36,5 +37,6 @@ class Client(client.Client):
36 37
             **kwargs
37 38
         )
38 39
 
40
+        self.dataframes = dataframes.DataframesManager(self.api_client)
39 41
         self.scope = scope.ScopeManager(self.api_client)
40 42
         self.summary = summary.SummaryManager(self.api_client)

+ 51
- 0
cloudkittyclient/v2/dataframes.py View File

@@ -0,0 +1,51 @@
1
+# Copyright 2019 Objectif Libre
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+#
15
+import json
16
+import six
17
+
18
+from cloudkittyclient.common import base
19
+from cloudkittyclient import exc
20
+
21
+
22
+class DataframesManager(base.BaseManager):
23
+    """Class used to handle /v2/dataframes endpoint"""
24
+
25
+    url = '/v2/dataframes'
26
+
27
+    def add_dataframes(self, **kwargs):
28
+        """Add DataFrames to the storage backend. Returns nothing.
29
+
30
+        :param dataframes: List of dataframes to add to the storage backend.
31
+        :type dataframes: list of dataframes
32
+        """
33
+
34
+        dataframes = kwargs.get('dataframes')
35
+
36
+        if not dataframes:
37
+            raise exc.ArgumentRequired("'dataframes' argument is required")
38
+
39
+        if not isinstance(dataframes, six.string_types):
40
+            try:
41
+                dataframes = json.dumps(dataframes)
42
+            except TypeError:
43
+                raise exc.InvalidArgumentError(
44
+                    "'dataframes' must be either a string"
45
+                    "or a JSON serializable object.")
46
+
47
+        url = self.get_url(None, kwargs)
48
+        return self.api_client.post(
49
+            url,
50
+            data=dataframes,
51
+        )

+ 42
- 0
cloudkittyclient/v2/dataframes_cli.py View File

@@ -0,0 +1,42 @@
1
+# Copyright 2019 Objectif Libre
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+#
15
+import argparse
16
+
17
+from cliff import command
18
+
19
+from cloudkittyclient import utils
20
+
21
+
22
+class CliDataframesAdd(command.Command):
23
+    """Add one or several DataFrame objects to the storage backend."""
24
+    def get_parser(self, prog_name):
25
+        parser = super(CliDataframesAdd, self).get_parser(prog_name)
26
+
27
+        parser.add_argument(
28
+            'datafile',
29
+            type=argparse.FileType('r'),
30
+            help="File formatted as a JSON object having a DataFrame list"
31
+                 "under a 'dataframes' key."
32
+                 "'-' (hyphen) can be specified for using stdin.",
33
+            )
34
+
35
+        return parser
36
+
37
+    def take_action(self, parsed_args):
38
+        with parsed_args.datafile as dfile:
39
+            dataframes = dfile.read()
40
+            utils.get_client_from_osc(self).dataframes.add_dataframes(
41
+                dataframes=dataframes,
42
+            )

+ 6
- 0
doc/source/api_reference/v2/dataframes.rst View File

@@ -0,0 +1,6 @@
1
+===========================
2
+dataframes (/v2/dataframes)
3
+===========================
4
+
5
+.. automodule:: cloudkittyclient.v2.dataframes
6
+   :members:

+ 3
- 0
doc/source/cli_reference.rst View File

@@ -12,6 +12,9 @@ V1 Client
12 12
 V2 Client
13 13
 =========
14 14
 
15
+.. autoprogram-cliff:: cloudkittyclient.v2
16
+   :command: dataframes add
17
+
15 18
 .. autoprogram-cliff:: cloudkittyclient.v2
16 19
    :command: scope state get
17 20
 

+ 5
- 0
releasenotes/notes/add-support-v2-dataframes-baa398fe5ea1b891.yaml View File

@@ -0,0 +1,5 @@
1
+---
2
+features:
3
+  - |
4
+    Support for the ``/v2/dataframes`` endpoint has been added to the client.
5
+    A new ``dataframes add`` CLI command is also available.

+ 4
- 0
setup.cfg View File

@@ -86,6 +86,8 @@ openstack.rating.v1 =
86 86
     rating_pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript
87 87
 
88 88
 openstack.rating.v2 =
89
+    rating_dataframes_add = cloudkittyclient.v2.dataframes_cli:CliDataframesAdd
90
+
89 91
     rating_scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet
90 92
     rating_scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset
91 93
 
@@ -200,6 +202,8 @@ cloudkittyclient.v1 =
200 202
     pyscript_delete = cloudkittyclient.v1.rating.pyscripts_cli:CliDeleteScript
201 203
 
202 204
 cloudkittyclient.v2 =
205
+    dataframes_add = cloudkittyclient.v2.dataframes_cli:CliDataframesAdd
206
+
203 207
     scope_state_get = cloudkittyclient.v2.scope_cli:CliScopeStateGet
204 208
     scope_state_reset = cloudkittyclient.v2.scope_cli:CliScopeStateReset
205 209
 

Loading…
Cancel
Save