├── .gitignore ├── LICENSE ├── README.md ├── Sandbox.xml └── piwebapi-examples.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | .idea/ 63 | 64 | 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 OSIsoft, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | These examples show how to browse the AF object hierarchy, read/write values, and update attribute metadata. 4 | 5 | For these examples, I've created an AF attribute at the path `\\SECRETAFSERVER\Sandbox\MyElement|MyAttribute`. 6 | The attribute is configured as a PI Point DR. 7 | 8 | I've set these variables in the code as shown below: 9 | 10 | ```Python 11 | pi_webapi_server = 'SECRETWEBSERVER' 12 | pi_asset_server = 'SECRETAFSERVER' 13 | pi_asset_database = 'Sandbox' 14 | ``` 15 | 16 | In the header, I've imported the relevant packages and functions that I need. 17 | 18 | ```Python 19 | import requests as req 20 | import json 21 | from bunch import bunchify, unbunchify 22 | ``` 23 | 24 | * [requests](http://docs.python-requests.org/en/latest/) is used as the HTTP client library. 25 | * [json](https://docs.python.org/2/library/json.html) as the name suggests helps me deserialize JSON text into Python 26 | dictionaries and vice versa. 27 | * [bunch](https://pypi.python.org/pypi/bunch/1.0.1) is a package that provides a wrapper class around Python dictionaries 28 | so I can browse the dictionary using dot notation (e.g. dict.key instead of dict["key"]), evocative of the C# anonymous 29 | type. 30 | 31 | The example file is structured with a set of helper functions in the beginning and the usage of these functions 32 | afterward. These helper functions are merely used to encapsulate basic operations on AF objects and hide away some 33 | implementation details. These functions do not represent best practices or offer a guide for designing Python wrappers 34 | for PI Web API calls. My experience with Python can be measured in units of days, rather than years... 35 | 36 | ## Examples 37 | 38 | Follow along in the code in [piwebapi-examples.py](https://github.com/bzshang/piwebapi-python-examples/blob/master/piwebapi-examples.py). 39 | 40 | ### 1.0 Get the root level PI Web API response 41 | 42 | To make things transparent, I will show how to get the root level response from a PI Web API call, which can be obtained 43 | also by going to `https:///piwebapi/` in the browser. 44 | 45 | I make the call 46 | 47 | ```Python 48 | pi_webapi_root = get_pi_webapi_root(pi_webapi_server) 49 | ``` 50 | 51 | Now, let's see what the `get_pi_webapi_root()` function does. 52 | 53 | ```Python 54 | def get_pi_webapi_root(webapi_server): 55 | # verify=False is to ignore SSL verification but this will vary depending on environment 56 | root_response = req.get('https://' + webapi_server + '/piwebapi', verify=False) 57 | # deserialize json into python dictionary. then convert to dot-accessible dictionary 58 | return bunchify(json.loads(root_response.text)) 59 | ``` 60 | 61 | It accepts the name of the web API server. Then, I issue an HTTP GET to the base URL using `req.get()` 62 | 63 | ```Python 64 | root_response = req.get('https://' + webapi_server + '/piwebapi', verify=False) 65 | ``` 66 | 67 | ``` 68 | Note that the full function call is requests.get(), but because I've imported the package using 69 | 'import requests as req', I can use this naming shortcut. 70 | ``` 71 | 72 | The `verify=False` is to ignore SSL certificate validation by the client, as in this case, I have a self-signed 73 | certificate. 74 | 75 | The `root_response` object contains the details of the HTTP response, such as the status code, response body, headers, 76 | cookies, etc. See the [reqests documentation](http://docs.python-requests.org/en/latest/user/quickstart/) 77 | for more details about this object. 78 | 79 | `root_response.text` returns the response body as a string. I want to convert this into a Python dictionary, so I can 80 | more easily work with the response and not worry about parsing JSON strings. `json.loads()` allows me to do this. 81 | However, I am also greedy (or lazy) :wink:and don't want to access the response type using `dict["Key"]` syntax. 82 | Instead, I prefer `dict.Key`, evocative of C# anonymous types. The [`bunch`](https://pypi.python.org/pypi/bunch/1.0.1) 83 | library allows me to do this, and `bunchify()` converts the ordinary dictionary into a dot-accessible dictionary. 84 | 85 | ### 2.0 Get AF server 86 | 87 | Now that I have the root level response (as a dot-accessible dictionary), I want to target a specific AF server. I use 88 | the helper function below to do so. 89 | 90 | ```Python 91 | af_server = get_asset_server(pi_webapi_root, pi_asset_server) 92 | ``` 93 | 94 | The function accepts the root object and also the name of the AF server I want. Let's look at this function. 95 | 96 | ```Python 97 | def get_asset_server(webapi_root_dict, asset_server): 98 | asset_servers_response = req.get(webapi_root_dict.Links.AssetServers, verify=False) 99 | asset_servers_dict = bunchify(json.loads(asset_servers_response.text)) 100 | asset_server_dict = next((x for x in asset_servers_dict.Items if x.Name == asset_server), None) 101 | return bunchify(asset_server_dict) 102 | ``` 103 | 104 | My ultimate goal is to obtain an object representing the target AF server. From the root dictionary, I issue an HTTP 105 | GET, passing in the URL `webapi_root_dict.Links.AssetServers`, whichs returns a JSON with a list of 106 | available AF servers. Again, I also `bunchify()` the response into a dot-accessible dictionary. 107 | 108 | Now, I need just the part of the dictionary that contains the AF server I'm interested in. I will use the `next()` 109 | function to retrieve the first matching entry in the list of AF servers, and default to `None`. I could have written the 110 | `for` loop and `if` check on separate lines, but I just want to show off Python's awesome support for [list 111 | comprehensions](https://docs.python.org/2/tutorial/datastructures.html). It is also more evocative of LINQ's 112 | `Select(x => x.Name == asset_server)` that I am accustomed to in C#. 113 | 114 | **Short diversion:** By using links, I'm inherently using the RESTful PI Web API's support for [HATEOAS] 115 | (http://en.wikipedia.org/wiki/HATEOAS) (Hypermedia as the Engine of Application State). I simply need to understand the 116 | media types and link relations among the response hypermedia, and can use hyperlinks to obtain other resources. Contrast 117 | this with SOAP, in which I would need to understand the interface contracts, object models, and application logic 118 | exposed by the service. 119 | 120 | 121 | ### 3.0 Get AF database, element, attribute 122 | 123 | Now that I have the AF server, I want to "drill down" to the AF attribute of interest. The rest of the functions are 124 | similar to what I did to grab the AF server. 125 | 126 | ```Python 127 | af_database = get_database(af_server, pi_asset_database) 128 | af_element = get_element(af_database, "MyElement") 129 | af_attribute = get_attribute(af_element, "MyAttribute") 130 | ``` 131 | 132 | You will notice `get_database()` is very similar to `get_asset_server()`. I wasn't kidding when I said I wasn't a Python 133 | developer, and that these helper functions do not expose elegant class library design... 134 | 135 | ### 4.0 Get AF attribute by path: Setting the query string in `requests` 136 | 137 | We made a lot of round-trips to the server just to get an attribute, which is a poor practice. What we could have done 138 | is get the attribute in one HTTP call by passing in the attribute path as a query string, using the PI Web API call 139 | `GET attributes`. See the [PI Web API Online Documentation](https://techsupport.osisoft.com/Documentation/PI-Web-API/help.html) for details. 140 | In Python, I use 141 | 142 | ```Python 143 | req_params = {'path': '\\\\SECRETAFSERVER\\SandBox\\MyElement|MyAttribute'} 144 | af_attribute = get_attribute_by_path(pi_webapi_root, req_params) 145 | ``` 146 | 147 | First, I set the query string parameters by creating a Python dictionary to store them. I created a helper function 148 | `get_attribute_by_path()` to find the attribute based on path. Here is that function. 149 | 150 | ```Python 151 | def get_attribute_by_path(webapi_root_dict, params): 152 | attribute_url = webapi_root_dict.Links.Self + 'attributes' 153 | asset_attributes_response = req.get(attribute_url, params=params, verify=False) 154 | return bunchify(json.loads(asset_attributes_response.text)) 155 | ``` 156 | 157 | Using `req.get()` from the `requests` package, I simply issue a GET request passing in the URL 158 | `https://SECRETWEBSERVER/piwebapi/attributes`, and the query string as a function argument as `params=params`. The 159 | response is the same as if I went to 160 | 161 | ``` 162 | https://SECRETWEBSERVER/piwebapi/attributes?path=\\\\SECRETAFSERVER\\SandBox\\MyElement|MyAttribute 163 | ``` 164 | 165 | in the browser. 166 | 167 | Perhaps a better way to obtain the attribute is to use PI Indexed Search via `GET search/query`, but I will leave it up 168 | to the reader :wink: 169 | 170 | ### 5.0 Get the current value of MyAttribute 171 | 172 | I use a helper function `get_stream_value()` and `req.get()` function from the `requests` library. Nothing new here. 173 | 174 | ### 6.0 Write a value to MyAttribute: POST JSON using `requests` 175 | 176 | Something new here. Here is the code I use to formulate the request. 177 | 178 | ```Python 179 | req_data = {'Timestamp': '2015-06-03T00:00:00', 'Value': '25.0'} 180 | req_headers = {'Content-Type': 'application/json'} 181 | post_result = post_stream_value(af_attribute, req_data, req_headers) 182 | ``` 183 | 184 | First, I set the query string in the URL via the `req_data` dictionary I created. Then, I set the HTTP request header 185 | using the `req_headers` dictionary. I pass both of these variables into my helper function `post_stream_value` along with 186 | my (dot-accessible) AF attribute dictionary. Here is the helper function. 187 | 188 | ```Python 189 | def post_stream_value(af_attribute_dict, json_data, headers): 190 | attribute_value_response = req.post(af_attribute_dict.Links.Value, 191 | json=json_data, 192 | headers=headers, 193 | verify=False) 194 | return attribute_value_response 195 | ``` 196 | 197 | To issue a POST in `requests`, it is very simple. Just use `req.post()`. I pass in the relevant URL, JSON body, and 198 | header information. Note I am returning the raw response object, as there is no JSON returned and I can inspect the 199 | status code later using `print post_result.status_code`. 200 | 201 | Just as an additional check, I read back in the value I just wrote using `get_stream_value()` but this time pass in a 202 | query string denoting the timestamp. 203 | 204 | ### 7.0 Add a description to MyAttribute: PATCH using `requests` 205 | 206 | These examples would not be complete if I hadn't snuck in `Hello world` somewhere in here. So we will allow our 207 | attribute to introduce herself to the world. Here is how to do so. 208 | 209 | ```Python 210 | req_data = {'Description': 'Hello world'} 211 | req_headers = {'Content-Type': 'application/json'} 212 | patch_result = update_af_attribute(af_attribute, req_data, req_headers) 213 | ``` 214 | 215 | It is the same dog but maybe a new trick. I formulate the request JSON in `req_data`, set the header in `req_headers` 216 | and then call my helper function `update_af_attribute()`, shown below. 217 | 218 | ```Python 219 | attribute_update_response = req.patch(af_attribute_dict.Links.Self, 220 | json=json_data, 221 | headers=headers, 222 | verify=False) 223 | ``` 224 | 225 | To update attribute metadata, I need to issue an HTTP PATCH request, which I can do using `req.patch()`. Lastly, I read 226 | back in the attribute to verify that I've updated successfully, using my helper function `get_attribute()` and storing 227 | the result in `af_attribute`. Because of the work I've done to return a dot-accessible dictionary, I can easily inspect 228 | the attribute description simply via `.Description`. Hello world! 229 | 230 | ## Summary 231 | 232 | In these examples, we've demonstrated the basic usage of PI Web API with the `requests` package in Python. Being able to 233 | access PI System data within Python brings the rich features of Python into PI, such as its numerical and scientific libraries 234 | (numpy, scipy, pandas, scikit-learn, etc.) and also its popular web application framework (Django). 235 | 236 | For questions or comments, please visit the associated blog post in 237 | [PI Developers Club](https://pisquare.osisoft.com/community/developers-club/blog/2015/06/04/using-pi-web-api-with-python). 238 | 239 | -------------------------------------------------------------------------------- /Sandbox.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Sandbox 5 | 6 | MyElement 7 | 8 | MyAttribute 9 | false 10 | Single 11 | 12 | PI Point 13 | \\%server%\test_write 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /piwebapi-examples.py: -------------------------------------------------------------------------------- 1 | import requests as req 2 | import json 3 | from bunch import bunchify, unbunchify 4 | 5 | def get_pi_webapi_root(webapi_server): 6 | # verify=False is to ignore SSL verification but this will vary depending on environment 7 | root_response = req.get('https://' + webapi_server + '/piwebapi', verify=False) 8 | # deserialize json into python dictionary. then convert to dot-accessible dictionary 9 | return bunchify(json.loads(root_response.text)) 10 | 11 | def get_asset_server(webapi_root_dict, asset_server): 12 | asset_servers_response = req.get(webapi_root_dict.Links.AssetServers, verify=False) 13 | asset_servers_dict = bunchify(json.loads(asset_servers_response.text)) 14 | asset_server_dict = next((x for x in asset_servers_dict.Items if x.Name == asset_server), None) 15 | return bunchify(asset_server_dict) 16 | 17 | def get_database(asset_server_dict, asset_database): 18 | asset_databases_response = req.get(asset_server_dict.Links.Databases, verify=False) 19 | asset_databases_dict = bunchify(json.loads(asset_databases_response.text)) 20 | asset_database_dict = next((x for x in asset_databases_dict.Items if x.Name == asset_database), None) 21 | return bunchify(asset_database_dict) 22 | 23 | def get_element(asset_database_dict, element): 24 | asset_elements_response = req.get(asset_database_dict.Links.Elements, verify=False) 25 | asset_elements_dict = bunchify(json.loads(asset_elements_response.text)) 26 | asset_element_dict = next((x for x in asset_elements_dict.Items if x.Name == element), None) 27 | return bunchify(asset_element_dict) 28 | 29 | def get_attribute(asset_element_dict, attribute): 30 | asset_attributes_response = req.get(asset_element_dict.Links.Attributes, verify=False) 31 | asset_attributes_dict = bunchify(json.loads(asset_attributes_response.text)) 32 | asset_attribute_dict = next((x for x in asset_attributes_dict.Items if x.Name == attribute), None) 33 | return bunchify(asset_attribute_dict) 34 | 35 | def get_attribute_by_path(webapi_root_dict, params): 36 | attribute_url = webapi_root_dict.Links.Self + 'attributes' 37 | asset_attributes_response = req.get(attribute_url, params=params, verify=False) 38 | return bunchify(json.loads(asset_attributes_response.text)) 39 | 40 | def get_stream_value(af_attribute_dict, params): 41 | attribute_value_response = req.get(af_attribute_dict.Links.Value, params=params, verify=False) 42 | return bunchify(json.loads(attribute_value_response.text)) 43 | 44 | def post_stream_value(af_attribute_dict, json_data, headers): 45 | attribute_value_response = req.post(af_attribute_dict.Links.Value, 46 | json=json_data, 47 | headers=headers, 48 | verify=False) 49 | return attribute_value_response 50 | 51 | def update_af_attribute(af_attribute_dict, json_data, headers): 52 | attribute_update_response = req.patch(af_attribute_dict.Links.Self, 53 | json=json_data, 54 | headers=headers, 55 | verify=False) 56 | return attribute_update_response 57 | 58 | if __name__ == "__main__": 59 | 60 | pi_webapi_server = 'SECRETWEBSERVER' 61 | pi_asset_server = 'SECRETAFSERVER' 62 | pi_asset_database = 'Sandbox' 63 | 64 | # 1.0 -------------------------------------------------------------------------------------------------------------- 65 | # Get the root level PI Web API 66 | pi_webapi_root = get_pi_webapi_root(pi_webapi_server) 67 | print unbunchify(pi_webapi_root) 68 | 69 | # 2.0 -------------------------------------------------------------------------------------------------------------- 70 | # Get AF server 71 | af_server = get_asset_server(pi_webapi_root, pi_asset_server) 72 | 73 | # 3.0 -------------------------------------------------------------------------------------------------------------- 74 | # Get AF database from server 75 | af_database = get_database(af_server, pi_asset_database) 76 | 77 | # Get AF element from database 78 | af_element = get_element(af_database, "MyElement") 79 | 80 | # Get AF attribute from element 81 | af_attribute = get_attribute(af_element, "MyAttribute") 82 | 83 | # 4.0 -------------------------------------------------------------------------------------------------------------- 84 | # Retrieve the same attribute by path 85 | req_params = {'path': '\\\\SECRETAFSERVER\\SandBox\\MyElement|MyAttribute'} 86 | af_attribute = get_attribute_by_path(pi_webapi_root, req_params) 87 | 88 | # 5.0 -------------------------------------------------------------------------------------------------------------- 89 | # Get AF value for attribute 90 | af_value = get_stream_value(af_attribute, None) 91 | 92 | print unbunchify(af_value) 93 | # output: 94 | # {u'Questionable': False, 95 | # u'Good': True, 96 | # u'UnitsAbbreviation': u'', 97 | # u'Timestamp': u'2015-06-02T21:05:19Z', 98 | # u'Value': 1.0, 99 | # u'Substituted': False} 100 | 101 | # 6.0 -------------------------------------------------------------------------------------------------------------- 102 | # Write a value to MyAttribute 103 | req_data = {'Timestamp': '2015-06-03T00:00:00', 'Value': '25.0'} 104 | req_headers = {'Content-Type': 'application/json'} 105 | post_result = post_stream_value(af_attribute, req_data, req_headers) 106 | print post_result.status_code 107 | # output: 108 | # 202 109 | 110 | # Read back the value just written 111 | req_params = {'time': '2015-06-03T00:00:00'} 112 | af_value = get_stream_value(af_attribute, req_params) 113 | print unbunchify(af_value) 114 | # output: 115 | # {u'Questionable': False, 116 | # u'Good': True, 117 | # u'UnitsAbbreviation': u'', 118 | # u'Timestamp': u'2015-06-02T21:05:19Z', 119 | # u'Value': 1.0, 120 | # u'Substituted': False} 121 | 122 | # 7.0 -------------------------------------------------------------------------------------------------------------- 123 | # Add a description to MyAttribute 124 | req_data = {'Description': 'Hello world'} 125 | req_headers = {'Content-Type': 'application/json'} 126 | patch_result = update_af_attribute(af_attribute, req_data, req_headers) 127 | print patch_result.status_code 128 | # output: 129 | # 204 130 | 131 | # Read the attribute description 132 | af_attribute = get_attribute(af_element, "MyAttribute") 133 | print af_attribute.Description 134 | # output: 135 | # Hello world 136 | 137 | 138 | 139 | 140 | --------------------------------------------------------------------------------