unitypredict-engines 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unitypredict-engines-0.0.1/PKG-INFO +194 -0
- unitypredict-engines-0.0.1/README.md +189 -0
- unitypredict-engines-0.0.1/setup.cfg +4 -0
- unitypredict-engines-0.0.1/setup.py +24 -0
- unitypredict-engines-0.0.1/unitypredict_engines/Models.py +110 -0
- unitypredict-engines-0.0.1/unitypredict_engines/Platform.py +109 -0
- unitypredict-engines-0.0.1/unitypredict_engines/UnityPredictLocalHost.py +460 -0
- unitypredict-engines-0.0.1/unitypredict_engines/__init__.py +1 -0
- unitypredict-engines-0.0.1/unitypredict_engines/scripts.py +5 -0
- unitypredict-engines-0.0.1/unitypredict_engines.egg-info/PKG-INFO +194 -0
- unitypredict-engines-0.0.1/unitypredict_engines.egg-info/SOURCES.txt +12 -0
- unitypredict-engines-0.0.1/unitypredict_engines.egg-info/dependency_links.txt +1 -0
- unitypredict-engines-0.0.1/unitypredict_engines.egg-info/entry_points.txt +2 -0
- unitypredict-engines-0.0.1/unitypredict_engines.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: unitypredict-engines
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Description-Content-Type: text/markdown
|
|
5
|
+
|
|
6
|
+
# UnityPredict Local App Engine Creator
|
|
7
|
+
|
|
8
|
+
## Introduction
|
|
9
|
+
|
|
10
|
+
The `UnityPredict` python sdk is designed to help accelerate the testing and debugging of **App Engines** for deployment on the `UnityPredict` platform.
|
|
11
|
+
|
|
12
|
+
On `UnityPredict`, **"Engines"** are the underlying compute framework that is executed, at scale, to perform inference or run business logic. In contrast, **"Models"** define the interface for these Engines. **Every Engine must be connected to a "Model"** because the Model serves as the interface that defines how `UnityPredict` communicates with the Engine. The Model specifies variable names and data types for inputs and outputs. Additionally, `UnityPredict` uses the Model definition to auto-generate APIs and user interfaces.
|
|
13
|
+
|
|
14
|
+
**"App Engines"** are specialized extensions of `UnityPredict` Engines that allow developers to write custom Python code, which the platform will execute at scale. These custom-defined Engines offer developers the flexibility needed to create complex applications. Within an App Engine, developers can access various platform features through code. For instance, **App Engine code can easily invoke other models (aka. chaining) or define cost calculations**. App Engines also enable developers to choose specific hardware types for running their code.
|
|
15
|
+
|
|
16
|
+
This guide focuses on the local development and testing of custom App Engine code.
|
|
17
|
+
|
|
18
|
+
For a full guide on how to use App Engine(s) on UnityPredict, please visit our complete help documentation here [UnityPredict Docs](https://console.unitypredict.com).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
* You can use pip to install the ```unitypredict``` library.
|
|
22
|
+
```bash
|
|
23
|
+
pip install unitypredict
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### EntryPoint.py
|
|
29
|
+
|
|
30
|
+
#### What is `EntryPoint.py`
|
|
31
|
+
|
|
32
|
+
`EntryPoint.py` is the script created by the **user** to provide the platform a well-defined function that can be invoked as the **starting point** for the **inference code**. It also acts as a gateway to access the **"platform"** object containing various features provided by UnityPredict
|
|
33
|
+
|
|
34
|
+
### Create an example `EntryPoint.py`
|
|
35
|
+
|
|
36
|
+
Create a custom entrypoint of your App Engine under the file **`EntryPoint.py`** containing the inference logic.
|
|
37
|
+
|
|
38
|
+
**NOTE:** The name of the file should be **`EntryPoint.py`**. If not followed, during the actual deployment on `UnityPredict` repository, the created file might not get invoked due to anincorrect name.
|
|
39
|
+
|
|
40
|
+
Given below is an example of a simple **`EntryPoint.py`**
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from unitypredict.Platform import IPlatform, InferenceRequest, InferenceResponse, OutcomeValue, InferenceContextData
|
|
45
|
+
import datetime
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_engine(request: InferenceRequest, platform: IPlatform) -> InferenceResponse:
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
platform.logMsg("Running User Code...")
|
|
54
|
+
response = InferenceResponse()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
prompt = request.InputValues['InputMessage']
|
|
58
|
+
|
|
59
|
+
currentExecTime = datetime.datetime.now()
|
|
60
|
+
currentExecTime = currentExecTime.strftime("%d-%m-%YT%H-%M-%S")
|
|
61
|
+
resp_message = "Echo message: {} Time:: {}".format(prompt, currentExecTime)
|
|
62
|
+
|
|
63
|
+
response.Outcomes['OutputMessage'] = [OutcomeValue(value=resp_message, probability=1.0)]
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
response.ErrorMessages = "Entrypoint Exception Occured: {}".format(str(e))
|
|
67
|
+
|
|
68
|
+
print("Finished Running User Code...")
|
|
69
|
+
return response
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Running the `EntryPoint.py`
|
|
74
|
+
|
|
75
|
+
In order to run the `EntryPoint.py` locally, add the following `main` section to the file itself.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
#### For Local testing ###
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
|
|
82
|
+
from unitypredict import UnityPredictHost
|
|
83
|
+
|
|
84
|
+
# Initialize the UnityPredict Platform
|
|
85
|
+
platform = UnityPredictHost()
|
|
86
|
+
|
|
87
|
+
testRequest = InferenceRequest()
|
|
88
|
+
testRequest.InputValues = {}
|
|
89
|
+
testRequest.InputValues["InputMessage"] = "Hi, this is an echo message"
|
|
90
|
+
results: InferenceResponse = run_engine(testRequest, platform)
|
|
91
|
+
|
|
92
|
+
# Print Outputs
|
|
93
|
+
if (results.Outcomes != None):
|
|
94
|
+
for outcomKeys, outcomeValues in results.Outcomes.items():
|
|
95
|
+
print ("\n\nOutcome Key: {}".format(outcomKeys))
|
|
96
|
+
for values in outcomeValues:
|
|
97
|
+
infVal: OutcomeValue = values
|
|
98
|
+
print ("Outcome Value: \n{}\n\n".format(infVal.Value))
|
|
99
|
+
print ("Outcome Probability: \n{}\n\n".format(infVal.Probability))
|
|
100
|
+
|
|
101
|
+
# Print Error Messages (if any)
|
|
102
|
+
print ("Error Messages: {}".format(results.ErrorMessages))
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Now run the `EntryPoint.py`
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python EntryPoint.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Output
|
|
112
|
+
|
|
113
|
+
The output should be the message added as the `InputMessage` in the main, appended with the timestamp of the execution.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
Config file detected, loading data from: D:\Documents\SelfProjects\UPTAzure\unitypredict-sdks\mainFolder\config.json
|
|
117
|
+
|
|
118
|
+
Running User Code...
|
|
119
|
+
|
|
120
|
+
Finished Running User Code...
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
Outcome Key: OutputMessage
|
|
124
|
+
Outcome Value:
|
|
125
|
+
Echo message: Hi, this is an echo message Time:: 18-08-2024T18-56-33
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
Outcome Probability:
|
|
129
|
+
1.0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
While running the script for the **first time**, you may also encounter a log message as follows.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
Config file not detected, creating templated config file: {project-current-folder}\config.json
|
|
137
|
+
```
|
|
138
|
+
This marks the auto-creation of the config file template `config.json`, along with the tree-structure following the templated config of the project which is the **mock building block** for the APIs of `UnityPredict` used under `EntryPoint.py`.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
### Config File and the project tree structure
|
|
143
|
+
|
|
144
|
+
The templated `config.json` looks like this:
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"ModelsDirPath": "{userPWD}/unitypredict_mocktool/models",
|
|
148
|
+
"RequestFilesDirPath": "{userPWD}/unitypredict_mocktool/requests",
|
|
149
|
+
"SAVE_CONTEXT": true,
|
|
150
|
+
"TempDirPath": "{userPWD}/unitypredict_mocktool/tmp",
|
|
151
|
+
"UPT_API_KEY": ""
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
And the tree structure of the generated directory will be:
|
|
156
|
+
```plaintext
|
|
157
|
+
{userPWD}/
|
|
158
|
+
├── unitypredict_mocktool/
|
|
159
|
+
| ├── models/
|
|
160
|
+
| ├── requests/
|
|
161
|
+
| └── tmp/
|
|
162
|
+
└── EntryPoint.py
|
|
163
|
+
└── config.json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Details of the JSON settings:
|
|
167
|
+
|
|
168
|
+
* **ModelsDirPath**: Local model files/binaries to be uploaded to AppEngine for using during the execution can be added under the specified **ModelsDirPath**
|
|
169
|
+
|
|
170
|
+
* **RequestFilesDirPath**: Files or Folders to be uploaded to AppEngine for using during the execution can be added under the specified **RequestFilesDirPath**
|
|
171
|
+
|
|
172
|
+
* **SAVE_CONTEXT**: Retains context across multiple requests. Disable it using ```"SAVE_CONTEXT" : false```
|
|
173
|
+
|
|
174
|
+
* **UPT_API_KEY**: API Key token generated from the UnityPredict profile of the user.
|
|
175
|
+
|
|
176
|
+
* **TempDirPath**: Temporary directory which can be used for writing or reading extra files or folders depending on the requirements of the App Engine
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
Copyright 2013 Mir Ikram Uddin
|
|
183
|
+
|
|
184
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
185
|
+
you may not use this file except in compliance with the License.
|
|
186
|
+
You may obtain a copy of the License at
|
|
187
|
+
|
|
188
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
189
|
+
|
|
190
|
+
Unless required by applicable law or agreed to in writing, software
|
|
191
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
192
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
193
|
+
See the License for the specific language governing permissions and
|
|
194
|
+
limitations under the License.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# UnityPredict Local App Engine Creator
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
The `UnityPredict` python sdk is designed to help accelerate the testing and debugging of **App Engines** for deployment on the `UnityPredict` platform.
|
|
6
|
+
|
|
7
|
+
On `UnityPredict`, **"Engines"** are the underlying compute framework that is executed, at scale, to perform inference or run business logic. In contrast, **"Models"** define the interface for these Engines. **Every Engine must be connected to a "Model"** because the Model serves as the interface that defines how `UnityPredict` communicates with the Engine. The Model specifies variable names and data types for inputs and outputs. Additionally, `UnityPredict` uses the Model definition to auto-generate APIs and user interfaces.
|
|
8
|
+
|
|
9
|
+
**"App Engines"** are specialized extensions of `UnityPredict` Engines that allow developers to write custom Python code, which the platform will execute at scale. These custom-defined Engines offer developers the flexibility needed to create complex applications. Within an App Engine, developers can access various platform features through code. For instance, **App Engine code can easily invoke other models (aka. chaining) or define cost calculations**. App Engines also enable developers to choose specific hardware types for running their code.
|
|
10
|
+
|
|
11
|
+
This guide focuses on the local development and testing of custom App Engine code.
|
|
12
|
+
|
|
13
|
+
For a full guide on how to use App Engine(s) on UnityPredict, please visit our complete help documentation here [UnityPredict Docs](https://console.unitypredict.com).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
* You can use pip to install the ```unitypredict``` library.
|
|
17
|
+
```bash
|
|
18
|
+
pip install unitypredict
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### EntryPoint.py
|
|
24
|
+
|
|
25
|
+
#### What is `EntryPoint.py`
|
|
26
|
+
|
|
27
|
+
`EntryPoint.py` is the script created by the **user** to provide the platform a well-defined function that can be invoked as the **starting point** for the **inference code**. It also acts as a gateway to access the **"platform"** object containing various features provided by UnityPredict
|
|
28
|
+
|
|
29
|
+
### Create an example `EntryPoint.py`
|
|
30
|
+
|
|
31
|
+
Create a custom entrypoint of your App Engine under the file **`EntryPoint.py`** containing the inference logic.
|
|
32
|
+
|
|
33
|
+
**NOTE:** The name of the file should be **`EntryPoint.py`**. If not followed, during the actual deployment on `UnityPredict` repository, the created file might not get invoked due to anincorrect name.
|
|
34
|
+
|
|
35
|
+
Given below is an example of a simple **`EntryPoint.py`**
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from unitypredict.Platform import IPlatform, InferenceRequest, InferenceResponse, OutcomeValue, InferenceContextData
|
|
40
|
+
import datetime
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_engine(request: InferenceRequest, platform: IPlatform) -> InferenceResponse:
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
platform.logMsg("Running User Code...")
|
|
49
|
+
response = InferenceResponse()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
prompt = request.InputValues['InputMessage']
|
|
53
|
+
|
|
54
|
+
currentExecTime = datetime.datetime.now()
|
|
55
|
+
currentExecTime = currentExecTime.strftime("%d-%m-%YT%H-%M-%S")
|
|
56
|
+
resp_message = "Echo message: {} Time:: {}".format(prompt, currentExecTime)
|
|
57
|
+
|
|
58
|
+
response.Outcomes['OutputMessage'] = [OutcomeValue(value=resp_message, probability=1.0)]
|
|
59
|
+
|
|
60
|
+
except Exception as e:
|
|
61
|
+
response.ErrorMessages = "Entrypoint Exception Occured: {}".format(str(e))
|
|
62
|
+
|
|
63
|
+
print("Finished Running User Code...")
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Running the `EntryPoint.py`
|
|
69
|
+
|
|
70
|
+
In order to run the `EntryPoint.py` locally, add the following `main` section to the file itself.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
#### For Local testing ###
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
|
|
77
|
+
from unitypredict import UnityPredictHost
|
|
78
|
+
|
|
79
|
+
# Initialize the UnityPredict Platform
|
|
80
|
+
platform = UnityPredictHost()
|
|
81
|
+
|
|
82
|
+
testRequest = InferenceRequest()
|
|
83
|
+
testRequest.InputValues = {}
|
|
84
|
+
testRequest.InputValues["InputMessage"] = "Hi, this is an echo message"
|
|
85
|
+
results: InferenceResponse = run_engine(testRequest, platform)
|
|
86
|
+
|
|
87
|
+
# Print Outputs
|
|
88
|
+
if (results.Outcomes != None):
|
|
89
|
+
for outcomKeys, outcomeValues in results.Outcomes.items():
|
|
90
|
+
print ("\n\nOutcome Key: {}".format(outcomKeys))
|
|
91
|
+
for values in outcomeValues:
|
|
92
|
+
infVal: OutcomeValue = values
|
|
93
|
+
print ("Outcome Value: \n{}\n\n".format(infVal.Value))
|
|
94
|
+
print ("Outcome Probability: \n{}\n\n".format(infVal.Probability))
|
|
95
|
+
|
|
96
|
+
# Print Error Messages (if any)
|
|
97
|
+
print ("Error Messages: {}".format(results.ErrorMessages))
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Now run the `EntryPoint.py`
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python EntryPoint.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Output
|
|
107
|
+
|
|
108
|
+
The output should be the message added as the `InputMessage` in the main, appended with the timestamp of the execution.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
Config file detected, loading data from: D:\Documents\SelfProjects\UPTAzure\unitypredict-sdks\mainFolder\config.json
|
|
112
|
+
|
|
113
|
+
Running User Code...
|
|
114
|
+
|
|
115
|
+
Finished Running User Code...
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
Outcome Key: OutputMessage
|
|
119
|
+
Outcome Value:
|
|
120
|
+
Echo message: Hi, this is an echo message Time:: 18-08-2024T18-56-33
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
Outcome Probability:
|
|
124
|
+
1.0
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
While running the script for the **first time**, you may also encounter a log message as follows.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
Config file not detected, creating templated config file: {project-current-folder}\config.json
|
|
132
|
+
```
|
|
133
|
+
This marks the auto-creation of the config file template `config.json`, along with the tree-structure following the templated config of the project which is the **mock building block** for the APIs of `UnityPredict` used under `EntryPoint.py`.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### Config File and the project tree structure
|
|
138
|
+
|
|
139
|
+
The templated `config.json` looks like this:
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"ModelsDirPath": "{userPWD}/unitypredict_mocktool/models",
|
|
143
|
+
"RequestFilesDirPath": "{userPWD}/unitypredict_mocktool/requests",
|
|
144
|
+
"SAVE_CONTEXT": true,
|
|
145
|
+
"TempDirPath": "{userPWD}/unitypredict_mocktool/tmp",
|
|
146
|
+
"UPT_API_KEY": ""
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
And the tree structure of the generated directory will be:
|
|
151
|
+
```plaintext
|
|
152
|
+
{userPWD}/
|
|
153
|
+
├── unitypredict_mocktool/
|
|
154
|
+
| ├── models/
|
|
155
|
+
| ├── requests/
|
|
156
|
+
| └── tmp/
|
|
157
|
+
└── EntryPoint.py
|
|
158
|
+
└── config.json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Details of the JSON settings:
|
|
162
|
+
|
|
163
|
+
* **ModelsDirPath**: Local model files/binaries to be uploaded to AppEngine for using during the execution can be added under the specified **ModelsDirPath**
|
|
164
|
+
|
|
165
|
+
* **RequestFilesDirPath**: Files or Folders to be uploaded to AppEngine for using during the execution can be added under the specified **RequestFilesDirPath**
|
|
166
|
+
|
|
167
|
+
* **SAVE_CONTEXT**: Retains context across multiple requests. Disable it using ```"SAVE_CONTEXT" : false```
|
|
168
|
+
|
|
169
|
+
* **UPT_API_KEY**: API Key token generated from the UnityPredict profile of the user.
|
|
170
|
+
|
|
171
|
+
* **TempDirPath**: Temporary directory which can be used for writing or reading extra files or folders depending on the requirements of the App Engine
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
Copyright 2013 Mir Ikram Uddin
|
|
178
|
+
|
|
179
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
180
|
+
you may not use this file except in compliance with the License.
|
|
181
|
+
You may obtain a copy of the License at
|
|
182
|
+
|
|
183
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
184
|
+
|
|
185
|
+
Unless required by applicable law or agreed to in writing, software
|
|
186
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
187
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
188
|
+
See the License for the specific language governing permissions and
|
|
189
|
+
limitations under the License.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
description = ""
|
|
4
|
+
with open("README.md", "r") as rdf:
|
|
5
|
+
description = rdf.read()
|
|
6
|
+
|
|
7
|
+
print ("Possible packages: {}".format(find_packages()))
|
|
8
|
+
|
|
9
|
+
setup (
|
|
10
|
+
name="unitypredict-engines",
|
|
11
|
+
version="0.0.1",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
install_requires=[
|
|
14
|
+
# Currently no dependencies
|
|
15
|
+
],
|
|
16
|
+
entry_points = { # this here is the magic that binds your function into a callable script
|
|
17
|
+
'console_scripts':
|
|
18
|
+
[
|
|
19
|
+
'unitypredict-engines=unitypredict_engines.scripts:main'
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
long_description=description,
|
|
23
|
+
long_description_content_type="text/markdown"
|
|
24
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import json
|
|
3
|
+
import attr
|
|
4
|
+
|
|
5
|
+
class EngineResults:
|
|
6
|
+
OutcomeValues: dict = {}
|
|
7
|
+
Outcomes: dict = {}
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.OutcomeValues = dict()
|
|
11
|
+
self.Outcomes = dict()
|
|
12
|
+
|
|
13
|
+
def toJSON(self):
|
|
14
|
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
|
15
|
+
|
|
16
|
+
class OutcomePrediction:
|
|
17
|
+
Probability = 0.0
|
|
18
|
+
Value = None
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.Probability = 0.0
|
|
22
|
+
self.Value = None
|
|
23
|
+
|
|
24
|
+
class DataTypes(str, Enum):
|
|
25
|
+
Boolean = 'Boolean'
|
|
26
|
+
Integer = 'Integer'
|
|
27
|
+
Float = 'Float'
|
|
28
|
+
String = 'String'
|
|
29
|
+
File = 'File'
|
|
30
|
+
Tensor = 'Tensor'
|
|
31
|
+
|
|
32
|
+
class InputInfo:
|
|
33
|
+
Name: str = ''
|
|
34
|
+
InputType: DataTypes = DataTypes.Integer
|
|
35
|
+
|
|
36
|
+
class OutcomeInfo:
|
|
37
|
+
Name: str = ''
|
|
38
|
+
OutcomeType: DataTypes = DataTypes.Integer
|
|
39
|
+
|
|
40
|
+
@attr.s(auto_attribs=True)
|
|
41
|
+
class BasePredictEngineConfig:
|
|
42
|
+
# Inputs: list[InputInfo] = None
|
|
43
|
+
# Outcomes: list[OutcomeInfo] = None
|
|
44
|
+
Inputs: list
|
|
45
|
+
Outcomes: list
|
|
46
|
+
|
|
47
|
+
@attr.s(auto_attribs=True)
|
|
48
|
+
class InferenceContext:
|
|
49
|
+
ContextId: str = ''
|
|
50
|
+
StoredMeta: dict = {}
|
|
51
|
+
|
|
52
|
+
def toJSON(self):
|
|
53
|
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
|
54
|
+
|
|
55
|
+
@attr.s(auto_attribs=True)
|
|
56
|
+
class EngineInputs:
|
|
57
|
+
InputValues: dict
|
|
58
|
+
DesiredOutcomes: list
|
|
59
|
+
|
|
60
|
+
# Note: these models will be different for every engine type
|
|
61
|
+
###############################################################################################################
|
|
62
|
+
@attr.s(auto_attribs=True)
|
|
63
|
+
class AppEngineInferenceOptions:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@attr.s(auto_attribs=True)
|
|
67
|
+
class AIEngineConfiguration (BasePredictEngineConfig):
|
|
68
|
+
InferenceOptions: AppEngineInferenceOptions = None
|
|
69
|
+
|
|
70
|
+
@attr.s(auto_attribs=True)
|
|
71
|
+
class AppEngineRequest:
|
|
72
|
+
RequestId: str = ''
|
|
73
|
+
EngineId: str = ''
|
|
74
|
+
RequestInputFiles: bool = False
|
|
75
|
+
RequestOutputFiles: bool = False
|
|
76
|
+
RequestFilesFolderPath: str = False
|
|
77
|
+
PackagesFolderPath: str = ''
|
|
78
|
+
PackagesFolderPath: str = ''
|
|
79
|
+
SourcesFolderPath: str = ''
|
|
80
|
+
ModelFilesFolderPath: str = ''
|
|
81
|
+
EngineApiKey: str = ''
|
|
82
|
+
PredictEndpoint: str = ''
|
|
83
|
+
Context: InferenceContext = None
|
|
84
|
+
CallbackQueue: str = ''
|
|
85
|
+
EngineInputData: EngineInputs = None
|
|
86
|
+
EngineConfig: AIEngineConfiguration = None
|
|
87
|
+
|
|
88
|
+
def toJSON(self):
|
|
89
|
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
|
90
|
+
|
|
91
|
+
###############################################################################################################
|
|
92
|
+
|
|
93
|
+
class UnityPredictEngineResponse:
|
|
94
|
+
RequestId: str = ''
|
|
95
|
+
ErrorMessages: str = ''
|
|
96
|
+
LogMessages: str = ''
|
|
97
|
+
AdditionalInferenceCosts: float = 0.0
|
|
98
|
+
EngineOutputs: EngineResults = None
|
|
99
|
+
Context: InferenceContext = None
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self.RequestId = ''
|
|
103
|
+
self.ErrorMessages = ''
|
|
104
|
+
self.LogMessages = ''
|
|
105
|
+
self.AdditionalInferenceCosts = 0.0
|
|
106
|
+
self.EngineOutputs = None
|
|
107
|
+
self.Context = None
|
|
108
|
+
|
|
109
|
+
def toJSON(self):
|
|
110
|
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from abc import ABCMeta, abstractmethod
|
|
2
|
+
from io import BufferedReader, IOBase
|
|
3
|
+
|
|
4
|
+
class OutcomeValue:
|
|
5
|
+
Probability = 0.0
|
|
6
|
+
Value = None
|
|
7
|
+
|
|
8
|
+
def __init__(self, value: any = '', probability: float = 0.0):
|
|
9
|
+
self.Probability = probability
|
|
10
|
+
self.Value = value
|
|
11
|
+
|
|
12
|
+
class InferenceContextData:
|
|
13
|
+
StoredMeta: dict = {}
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.StoredMeta = {}
|
|
17
|
+
|
|
18
|
+
class InferenceRequest:
|
|
19
|
+
InputValues: dict
|
|
20
|
+
DesiredOutcomes: list
|
|
21
|
+
Context: InferenceContextData = None
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.Context = InferenceContextData()
|
|
24
|
+
self.InputValues = {}
|
|
25
|
+
self.DesiredOutcomes = []
|
|
26
|
+
|
|
27
|
+
class InferenceResponse:
|
|
28
|
+
ErrorMessages: str = ''
|
|
29
|
+
AdditionalInferenceCosts: float = 0.0
|
|
30
|
+
Context: InferenceContextData = None
|
|
31
|
+
OutcomeValues: dict = {}
|
|
32
|
+
Outcomes: dict = {}
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.ErrorMessages = ''
|
|
36
|
+
self.AdditionalInferenceCosts = 0.0
|
|
37
|
+
self.Context = InferenceContextData()
|
|
38
|
+
self.OutcomeValues = {}
|
|
39
|
+
self.Outcomes = {}
|
|
40
|
+
|
|
41
|
+
class ChainedInferenceRequest:
|
|
42
|
+
ContextId: str = ''
|
|
43
|
+
InputValues: dict
|
|
44
|
+
DesiredOutcomes: list
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self.ContextId = ''
|
|
48
|
+
self.InputValues = {}
|
|
49
|
+
self.DesiredOutcomes = []
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ChainedInferenceResponse:
|
|
53
|
+
ContextId: str = ''
|
|
54
|
+
RequestId: str = ''
|
|
55
|
+
ErrorMessages: str = ''
|
|
56
|
+
ComputeCost: float = 0.0
|
|
57
|
+
OutcomeValues: dict = {}
|
|
58
|
+
Outcomes: dict = {}
|
|
59
|
+
|
|
60
|
+
def __init__(self):
|
|
61
|
+
self.ContextId = ''
|
|
62
|
+
self.RequestId = ''
|
|
63
|
+
self.ErrorMessages = ''
|
|
64
|
+
self.ComputeCost = 0.0
|
|
65
|
+
self.OutcomeValues = {}
|
|
66
|
+
self.Outcomes = {}
|
|
67
|
+
|
|
68
|
+
class FileTransmissionObj:
|
|
69
|
+
FileName: str = ''
|
|
70
|
+
FileHandle: IOBase = None
|
|
71
|
+
|
|
72
|
+
def __init__(self, fileName, fileHandle):
|
|
73
|
+
self.FileName = fileName
|
|
74
|
+
self.FileHandle = fileHandle
|
|
75
|
+
|
|
76
|
+
class FileReceivedObj:
|
|
77
|
+
FileName: str = ''
|
|
78
|
+
LocalFilePath: str = ''
|
|
79
|
+
|
|
80
|
+
def __init__(self, fileName, localFilePath):
|
|
81
|
+
self.FileName = fileName
|
|
82
|
+
self.LocalFilePath = localFilePath
|
|
83
|
+
|
|
84
|
+
class IPlatform:
|
|
85
|
+
__metaclass__ = ABCMeta
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def version(self): return "1.0"
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def getModelsFolderPath(self) -> str: raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def getModelFile(self, modelFileName: str, mode: str = 'rb') -> IOBase: raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def getRequestFile(self, modelFileName: str, mode: str = 'rb') -> IOBase: raise NotImplementedError
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def saveRequestFile(self, modelFileName: str, mode: str = 'wb') -> IOBase: raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def getLocalTempFolderPath(self) -> str: raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def logMsg(self, msg: str): raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def invokeUnityPredictModel(self, modelId: str, request: ChainedInferenceRequest) -> ChainedInferenceResponse: raise NotImplementedError
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from io import BufferedReader, IOBase, StringIO
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os, sys
|
|
6
|
+
import shutil
|
|
7
|
+
import datetime
|
|
8
|
+
import uuid
|
|
9
|
+
import filecmp
|
|
10
|
+
import attr
|
|
11
|
+
import cattr
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
from .Models import AppEngineRequest, EngineResults, InferenceContext, UnityPredictEngineResponse
|
|
15
|
+
from .Platform import ChainedInferenceRequest, ChainedInferenceResponse, FileReceivedObj, FileTransmissionObj, IPlatform, InferenceRequest,InferenceResponse, InferenceContextData
|
|
16
|
+
|
|
17
|
+
@attr.s(auto_attribs=True)
|
|
18
|
+
class AppEngineConfig:
|
|
19
|
+
TempDirPath: str = ''
|
|
20
|
+
ModelsDirPath: str = ''
|
|
21
|
+
RequestFilesDirPath: str = ''
|
|
22
|
+
|
|
23
|
+
# TEMP_EXEC_DIR: str = os.getcwd()
|
|
24
|
+
UPT_API_KEY: str = ""
|
|
25
|
+
# MODEL_DIR: str = os.path.join(os.getcwd(), "models")
|
|
26
|
+
# REQUEST_DIR: str = os.path.join(os.getcwd(), "requests")
|
|
27
|
+
SAVE_CONTEXT: bool = True
|
|
28
|
+
|
|
29
|
+
def toJSON(self):
|
|
30
|
+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)
|
|
31
|
+
|
|
32
|
+
class UnityPredictHost(IPlatform):
|
|
33
|
+
|
|
34
|
+
Initialized = False
|
|
35
|
+
isConfigInit = False
|
|
36
|
+
LoadedEngineId = None
|
|
37
|
+
CurrentRequest: AppEngineRequest = None
|
|
38
|
+
logMsgs: str = ''
|
|
39
|
+
|
|
40
|
+
configFile = os.path.join(os.getcwd(), "config.json")
|
|
41
|
+
|
|
42
|
+
appEngineConfig: AppEngineConfig = AppEngineConfig()
|
|
43
|
+
|
|
44
|
+
# execTempDir = "execTmp"
|
|
45
|
+
# execRequestFolder: str = "requests"
|
|
46
|
+
# execModelFolder: str = "models"
|
|
47
|
+
# execOutputFolder: str = "outputs"
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
|
|
51
|
+
config_dict : dict = {}
|
|
52
|
+
if not os.path.exists(self.configFile):
|
|
53
|
+
|
|
54
|
+
print ("Config file not detected, creating templated config file: {}".format(self.configFile))
|
|
55
|
+
|
|
56
|
+
# self.appEngineConfig.TEMP_EXEC_DIR = os.getcwd()
|
|
57
|
+
self.appEngineConfig = AppEngineConfig()
|
|
58
|
+
self.appEngineConfig.ModelsDirPath = os.path.join(os.getcwd(), "unitypredict_mocktool", "models")
|
|
59
|
+
self.appEngineConfig.TempDirPath = os.path.join(os.getcwd(), "unitypredict_mocktool", "tmp")
|
|
60
|
+
self.appEngineConfig.RequestFilesDirPath = os.path.join(os.getcwd(), "unitypredict_mocktool", "requests")
|
|
61
|
+
|
|
62
|
+
with open(self.configFile, "w+") as confFile:
|
|
63
|
+
confFile.write(self.appEngineConfig.toJSON())
|
|
64
|
+
|
|
65
|
+
print ("Config file detected, loading data from: {}".format(self.configFile))
|
|
66
|
+
with open (self.configFile, "r+") as confFile:
|
|
67
|
+
config_dict = json.load(confFile)
|
|
68
|
+
|
|
69
|
+
self.appEngineConfig = cattr.structure(config_dict, AppEngineConfig)
|
|
70
|
+
|
|
71
|
+
if not os.path.exists(self.appEngineConfig.ModelsDirPath):
|
|
72
|
+
os.makedirs(self.appEngineConfig.ModelsDirPath)
|
|
73
|
+
|
|
74
|
+
if not os.path.exists(self.appEngineConfig.TempDirPath):
|
|
75
|
+
os.makedirs(self.appEngineConfig.TempDirPath)
|
|
76
|
+
|
|
77
|
+
if not os.path.exists(self.appEngineConfig.RequestFilesDirPath):
|
|
78
|
+
os.makedirs(self.appEngineConfig.RequestFilesDirPath)
|
|
79
|
+
|
|
80
|
+
self.CurrentRequest = AppEngineRequest()
|
|
81
|
+
|
|
82
|
+
# self.workingDir = os.path.join(self.appEngineConfig.TEMP_EXEC_DIR, self.execTempDir)
|
|
83
|
+
self.CurrentRequest.EngineApiKey = self.appEngineConfig.UPT_API_KEY
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# self.CurrentRequest.ModelFilesFolderPath = os.path.join(self.workingDir, self.execModelFolder)
|
|
87
|
+
# self.CurrentRequest.RequestFilesFolderPath = os.path.join(self.workingDir, self.execRequestFolder)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
self.isConfigInit = True
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def isConfigInitialized(self) -> bool:
|
|
95
|
+
|
|
96
|
+
return self.isConfigInit
|
|
97
|
+
|
|
98
|
+
def isPlatformInitialized(self) -> bool:
|
|
99
|
+
|
|
100
|
+
return self.Initialized
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def getModelsFolderPath(self) -> str:
|
|
104
|
+
return self.appEngineConfig.ModelsDirPath
|
|
105
|
+
|
|
106
|
+
def getModelFile(self, modelFileName: str, mode: str = 'rb') -> IOBase:
|
|
107
|
+
absFilePath = os.path.join(self.getModelsFolderPath(), modelFileName)
|
|
108
|
+
fileHandler = open(absFilePath, mode)
|
|
109
|
+
|
|
110
|
+
return fileHandler
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def getLocalTempFolderPath(self) -> str:
|
|
114
|
+
return self.appEngineConfig.TempDirPath
|
|
115
|
+
|
|
116
|
+
def getRequestFolderPath(self) -> str:
|
|
117
|
+
return self.appEngineConfig.RequestFilesDirPath
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def getRequestFile(self, requestFileName: str, mode: str = 'rb') -> IOBase:
|
|
121
|
+
absFilePath = os.path.join(self.getRequestFolderPath(), requestFileName)
|
|
122
|
+
fileHandler = open(absFilePath, mode)
|
|
123
|
+
|
|
124
|
+
return fileHandler
|
|
125
|
+
|
|
126
|
+
def saveRequestFile(self, requestFileName: str, mode: str = 'wb') -> IOBase:
|
|
127
|
+
absFilePath = os.path.join(self.getRequestFolderPath(), requestFileName)
|
|
128
|
+
fileHandler = open(absFilePath, mode)
|
|
129
|
+
|
|
130
|
+
return fileHandler
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def logMsg(self, msg: str):
|
|
134
|
+
print("\n{}\n".format(msg))
|
|
135
|
+
self.logMsgs += "\n{}\n".format(msg)
|
|
136
|
+
|
|
137
|
+
def errorMsg(self, msg: str):
|
|
138
|
+
self.errorMsgs += "\n{}\n".format(msg)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# def syncDirectories(self, source: str, destination: str, create_dest_if_not_present: bool = False):
|
|
142
|
+
|
|
143
|
+
# if not os.path.exists(source) or not os.path.isdir(source):
|
|
144
|
+
# print(f"Source directory '{source}' does not exist or is not a directory.")
|
|
145
|
+
# return
|
|
146
|
+
|
|
147
|
+
# if create_dest_if_not_present:
|
|
148
|
+
# if not os.path.exists(destination):
|
|
149
|
+
# os.makedirs(destination)
|
|
150
|
+
# else:
|
|
151
|
+
# if not os.path.exists(destination) or not os.path.isdir(source):
|
|
152
|
+
# print(f"Destination directory '{destination}' does not exist or is not a directory.")
|
|
153
|
+
# return
|
|
154
|
+
|
|
155
|
+
# # Copy or update files from source to destination
|
|
156
|
+
# for src_dir, _, files in os.walk(source):
|
|
157
|
+
# dst_dir = src_dir.replace(source, destination, 1)
|
|
158
|
+
# if not os.path.exists(dst_dir):
|
|
159
|
+
# os.makedirs(dst_dir)
|
|
160
|
+
|
|
161
|
+
# for file in files:
|
|
162
|
+
# src_file = os.path.join(src_dir, file)
|
|
163
|
+
# dst_file = os.path.join(dst_dir, file)
|
|
164
|
+
|
|
165
|
+
# if not os.path.exists(dst_file) or not filecmp.cmp(src_file, dst_file, shallow=False):
|
|
166
|
+
# shutil.copy2(src_file, dst_file) # Preserve metadata with copy2
|
|
167
|
+
# print(f"Copied: {src_file} to {dst_file}")
|
|
168
|
+
# else:
|
|
169
|
+
# print(f"Skipped: {src_file} is already up to date.")
|
|
170
|
+
|
|
171
|
+
# # Remove files and directories from destination that are not in source
|
|
172
|
+
# for dst_dir, _, files in os.walk(destination, topdown=False):
|
|
173
|
+
# src_dir = dst_dir.replace(destination, source, 1)
|
|
174
|
+
|
|
175
|
+
# if not os.path.exists(src_dir):
|
|
176
|
+
# shutil.rmtree(dst_dir)
|
|
177
|
+
# print(f"Removed directory: {dst_dir}")
|
|
178
|
+
# else:
|
|
179
|
+
# for file in files:
|
|
180
|
+
# dst_file = os.path.join(dst_dir, file)
|
|
181
|
+
# src_file = os.path.join(src_dir, file)
|
|
182
|
+
|
|
183
|
+
# if not os.path.exists(src_file):
|
|
184
|
+
# os.remove(dst_file)
|
|
185
|
+
# print(f"Removed file: {dst_file}")
|
|
186
|
+
|
|
187
|
+
# def createPlatform(self, request: AppEngineRequest) -> bool:
|
|
188
|
+
# if not self.isConfigInit:
|
|
189
|
+
# return False
|
|
190
|
+
|
|
191
|
+
# # Creating output folder based on RequestID
|
|
192
|
+
# if (request.RequestId == ""):
|
|
193
|
+
|
|
194
|
+
# self.errorMsg("Please provide a proper request id\n")
|
|
195
|
+
|
|
196
|
+
# return False
|
|
197
|
+
|
|
198
|
+
# # Create the folders
|
|
199
|
+
# if not os.path.exists(self.workingDir):
|
|
200
|
+
# os.mkdir(self.workingDir)
|
|
201
|
+
|
|
202
|
+
# if not os.path.exists(self.CurrentRequest.ModelFilesFolderPath):
|
|
203
|
+
# os.mkdir(self.CurrentRequest.ModelFilesFolderPath)
|
|
204
|
+
|
|
205
|
+
# # Create request folder having the name of the current Timestamp
|
|
206
|
+
# currentExecTime = datetime.datetime.now()
|
|
207
|
+
# currentExecTime = currentExecTime.strftime("%d-%m-%YT%H-%M-%S")
|
|
208
|
+
|
|
209
|
+
# self.execRequestFolder = "{}_{}__{}".format(self.execRequestFolder, currentExecTime, request.RequestId)
|
|
210
|
+
# self.CurrentRequest.RequestFilesFolderPath = os.path.join(self.workingDir, self.execRequestFolder)
|
|
211
|
+
|
|
212
|
+
# if not os.path.exists(self.CurrentRequest.RequestFilesFolderPath):
|
|
213
|
+
# os.mkdir(self.CurrentRequest.RequestFilesFolderPath)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# # Create output folder based on the request Id
|
|
217
|
+
# self.execOutputFolder = "{}_{}__{}".format(self.execOutputFolder, currentExecTime, request.RequestId)
|
|
218
|
+
# self.execOutputFolder = os.path.join(self.workingDir, self.execOutputFolder)
|
|
219
|
+
# if not os.path.exists(self.execOutputFolder):
|
|
220
|
+
# os.mkdir(self.execOutputFolder)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# # TODO: Copy user configured folder contents to respective exec folders
|
|
224
|
+
# # Sync Request Files
|
|
225
|
+
# self.syncDirectories(self.appEngineConfig.REQUEST_DIR, self.CurrentRequest.RequestFilesFolderPath)
|
|
226
|
+
# # Sync Model Files
|
|
227
|
+
# self.syncDirectories(self.appEngineConfig.MODEL_DIR, self.CurrentRequest.ModelFilesFolderPath)
|
|
228
|
+
|
|
229
|
+
# self.Initialized = True
|
|
230
|
+
|
|
231
|
+
# return True
|
|
232
|
+
|
|
233
|
+
def run_engine(self, request: AppEngineRequest) -> UnityPredictEngineResponse:
|
|
234
|
+
|
|
235
|
+
self.logMsgs: str = ''
|
|
236
|
+
self.MaxlogMsgBuffer: int = 3000
|
|
237
|
+
self.NegMaxlogMsgBuffer: int = -1 * self.MaxlogMsgBuffer
|
|
238
|
+
|
|
239
|
+
self.errorMsgs: str = ''
|
|
240
|
+
self.MaxErrorMsgsBuffer: int = 3000
|
|
241
|
+
self.NegMaxErrorMsgsBuffer: int = -1 * self.MaxErrorMsgsBuffer
|
|
242
|
+
|
|
243
|
+
toreturn: UnityPredictEngineResponse = UnityPredictEngineResponse()
|
|
244
|
+
try:
|
|
245
|
+
|
|
246
|
+
# # Create the platform for execution
|
|
247
|
+
# # 1) Prepare temp folders on local directories for A) Request Files B) Model Files C) Temp Files
|
|
248
|
+
# # 2) Put the files into the right folders
|
|
249
|
+
# if not self.createPlatform(request=request):
|
|
250
|
+
|
|
251
|
+
# self.errorMsg("Unable to create the platform")
|
|
252
|
+
# toreturn.ErrorMessages = self.errorMsgs[self.NegMaxErrorMsgsBuffer:] # limit length of the logs
|
|
253
|
+
# toreturn.LogMessages = self.logMsgs[self.NegMaxlogMsgBuffer:] # limit length of the logs
|
|
254
|
+
|
|
255
|
+
# return toreturn
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# 3) Store/Restore Context (probably using some local json file) if requested by user
|
|
259
|
+
# Initialize context
|
|
260
|
+
|
|
261
|
+
if (self.appEngineConfig.SAVE_CONTEXT):
|
|
262
|
+
|
|
263
|
+
contextJson = os.path.join(self.getLocalTempFolderPath(), "context.json")
|
|
264
|
+
|
|
265
|
+
context = {}
|
|
266
|
+
if os.path.exists(contextJson):
|
|
267
|
+
with open(contextJson, "r+") as ctxtf:
|
|
268
|
+
context = json.load(ctxtf)
|
|
269
|
+
|
|
270
|
+
request.Context = InferenceContext(**context)
|
|
271
|
+
|
|
272
|
+
# 4) Convert the AppEngineRequest to the InferenceRequest that the model needs
|
|
273
|
+
|
|
274
|
+
inferReq: InferenceRequest = InferenceRequest()
|
|
275
|
+
inferReq.InputValues = request.EngineInputData.InputValues
|
|
276
|
+
inferReq.DesiredOutcomes = request.EngineInputData.DesiredOutcomes
|
|
277
|
+
|
|
278
|
+
inferReq.Context = InferenceContextData()
|
|
279
|
+
|
|
280
|
+
if (request.Context != None):
|
|
281
|
+
inferReq.Context.StoredMeta = request.Context.StoredMeta
|
|
282
|
+
else:
|
|
283
|
+
inferReq.Context.StoredMeta = {}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# 5) Run EntryPoint.py
|
|
287
|
+
entryPoint = importlib.import_module("EntryPoint")
|
|
288
|
+
inferResp: InferenceResponse = entryPoint.run_local_engine(inferReq, self)
|
|
289
|
+
|
|
290
|
+
# 6) Copy InferenceResponse to UnityPredictEngineResponse
|
|
291
|
+
|
|
292
|
+
toreturn.AdditionalInferenceCosts = inferResp.AdditionalInferenceCosts
|
|
293
|
+
toreturn.EngineOutputs = EngineResults()
|
|
294
|
+
|
|
295
|
+
if (inferResp.Outcomes != None):
|
|
296
|
+
toreturn.EngineOutputs.Outcomes = inferResp.Outcomes
|
|
297
|
+
else:
|
|
298
|
+
toreturn.EngineOutputs.Outcomes = {}
|
|
299
|
+
|
|
300
|
+
if (inferResp.OutcomeValues != None):
|
|
301
|
+
toreturn.EngineOutputs.OutcomeValues = inferResp.OutcomeValues
|
|
302
|
+
else:
|
|
303
|
+
toreturn.EngineOutputs.OutcomeValues = {}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
toreturn.EngineOutputs.OutcomeValues = inferResp.OutcomeValues
|
|
307
|
+
toreturn.ErrorMessages = inferResp.ErrorMessages
|
|
308
|
+
|
|
309
|
+
if (self.appEngineConfig.SAVE_CONTEXT):
|
|
310
|
+
|
|
311
|
+
toreturn.Context = InferenceContext(ContextId="")
|
|
312
|
+
if (request.Context != None):
|
|
313
|
+
toreturn.Context.ContextId = request.Context.ContextId
|
|
314
|
+
if (inferResp.Context == None or inferResp.Context.StoredMeta == None or inferResp.Context.StoredMeta == {}):
|
|
315
|
+
|
|
316
|
+
if (request.Context != None):
|
|
317
|
+
toreturn.Context.StoredMeta = inferReq.Context.StoredMeta
|
|
318
|
+
else:
|
|
319
|
+
toreturn.Context.StoredMeta = {}
|
|
320
|
+
|
|
321
|
+
else:
|
|
322
|
+
toreturn.Context.StoredMeta = inferResp.Context.StoredMeta
|
|
323
|
+
|
|
324
|
+
if toreturn.Context.ContextId == "":
|
|
325
|
+
toreturn.Context.ContextId = str(uuid.uuid4())
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
with open(contextJson, "w+") as ctxtf:
|
|
329
|
+
ctxtf.write(toreturn.Context.toJSON())
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
toreturn.LogMessages = self.logMsgs[self.NegMaxlogMsgBuffer:] # limit length of the logs
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
|
|
336
|
+
print ("Error occured: {}".format(str(e)))
|
|
337
|
+
self.errorMsg(str(e))
|
|
338
|
+
toreturn.ErrorMessages += self.errorMsgs[self.NegMaxErrorMsgsBuffer:] # limit length of the logs
|
|
339
|
+
toreturn.LogMessages = self.logMsgs[self.NegMaxlogMsgBuffer:] # limit length of the logs
|
|
340
|
+
|
|
341
|
+
return toreturn
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def invokeUnityPredictModel(self, modelId: str, request: ChainedInferenceRequest) -> ChainedInferenceResponse:
|
|
345
|
+
results = ChainedInferenceResponse()
|
|
346
|
+
|
|
347
|
+
apiKey = self.CurrentRequest.EngineApiKey
|
|
348
|
+
apiBaseUrl = "https://api.prod.unitypredict.com/api/predict"
|
|
349
|
+
|
|
350
|
+
needFileUpload: bool = False
|
|
351
|
+
|
|
352
|
+
#####
|
|
353
|
+
# The request can contain file objects so we need to change those to file names before sending out
|
|
354
|
+
#####
|
|
355
|
+
# first get a list of file that we'll need to upload later & update the POST obj
|
|
356
|
+
filesToUpload = {}
|
|
357
|
+
for xvarName in request.InputValues:
|
|
358
|
+
if isinstance(request.InputValues.get(xvarName), FileTransmissionObj):
|
|
359
|
+
needFileUpload = True
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
finalResponseJson: any = ''
|
|
363
|
+
|
|
364
|
+
response: requests.Response = None
|
|
365
|
+
if not needFileUpload:
|
|
366
|
+
# serialize the POST obj
|
|
367
|
+
jsonBody = json.dumps(request, default=vars)
|
|
368
|
+
|
|
369
|
+
# there are no files to upload so just post normally
|
|
370
|
+
response = requests.post(url = "{}/{}".format(apiBaseUrl, modelId), data=jsonBody, headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
371
|
+
|
|
372
|
+
if response.status_code != 200:
|
|
373
|
+
results.ErrorMessages = 'Error from server: {}'.format(response.status_code)
|
|
374
|
+
return results
|
|
375
|
+
|
|
376
|
+
finalResponseJson = response.json()
|
|
377
|
+
else:
|
|
378
|
+
# we need to initialize first
|
|
379
|
+
response = requests.post(url = "{}/initialize/{}".format(apiBaseUrl, modelId), data="", headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
380
|
+
|
|
381
|
+
requestId: str = response.json().get('requestId')
|
|
382
|
+
|
|
383
|
+
if response.status_code != 200:
|
|
384
|
+
results.ErrorMessages = 'Error from server: {}'.format(response.status_code)
|
|
385
|
+
return results
|
|
386
|
+
|
|
387
|
+
# upload the files
|
|
388
|
+
for xvarName in request.InputValues:
|
|
389
|
+
if isinstance(request.InputValues.get(xvarName), FileTransmissionObj):
|
|
390
|
+
fileToUpload: FileTransmissionObj = request.InputValues.get(xvarName)
|
|
391
|
+
response = requests.get(url = "{}/upload/{}/{}".format(apiBaseUrl, requestId, fileToUpload.FileName), headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
392
|
+
uploadLink = response.json().get('uploadLink')
|
|
393
|
+
fileName = response.json().get('fileName')
|
|
394
|
+
request.InputValues[xvarName] = fileName # make sure that only the filename is in the request that we are going to POST
|
|
395
|
+
requests.put(url = uploadLink, data=fileToUpload.FileHandle)
|
|
396
|
+
|
|
397
|
+
jsonBody = json.dumps(request, default=vars)
|
|
398
|
+
|
|
399
|
+
response = requests.post(url = "{}/{}/{}".format(apiBaseUrl, modelId, requestId), data=jsonBody, headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
400
|
+
|
|
401
|
+
finalResponseJson = response.json()
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
print(finalResponseJson)
|
|
405
|
+
|
|
406
|
+
while (finalResponseJson.get('status') == 'Processing'):
|
|
407
|
+
statusUrl: str = finalResponseJson.get('statusUrl')
|
|
408
|
+
|
|
409
|
+
response = requests.get(url = statusUrl, headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
410
|
+
finalResponseJson = response.json()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
finalResponseRequestId: str = finalResponseJson.get('requestId')
|
|
414
|
+
|
|
415
|
+
tempOutputFolder: str = os.path.join(self.getLocalTempFolderPath(), "chainedResults", finalResponseRequestId)
|
|
416
|
+
if not os.path.exists(tempOutputFolder):
|
|
417
|
+
os.makedirs(tempOutputFolder)
|
|
418
|
+
|
|
419
|
+
outcomeValues = finalResponseJson.get('outcomeValues')
|
|
420
|
+
for outputVarName in outcomeValues:
|
|
421
|
+
outcome: dict = outcomeValues.get(outputVarName)
|
|
422
|
+
if outcome.get('dataType') == 'File':
|
|
423
|
+
fileName = outcome.get('value')
|
|
424
|
+
|
|
425
|
+
tempFilePath = os.path.join(tempOutputFolder, fileName)
|
|
426
|
+
response = requests.get(url = "{}/download/{}/{}".format(apiBaseUrl, finalResponseRequestId, fileName), headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
427
|
+
with open(tempFilePath, 'wb') as f:
|
|
428
|
+
f.write(response.content)
|
|
429
|
+
|
|
430
|
+
fileReceived: FileReceivedObj = FileReceivedObj(fileName, tempFilePath)
|
|
431
|
+
outcome['value'] = fileReceived
|
|
432
|
+
|
|
433
|
+
outcomes = finalResponseJson.get('outcomes')
|
|
434
|
+
for outputVarName in outcomes:
|
|
435
|
+
outcome: list = outcomes.get(outputVarName)
|
|
436
|
+
for outcomeItem in outcome:
|
|
437
|
+
if outcomeItem.get('dataType') == 'File':
|
|
438
|
+
fileName = outcomeItem.get('value')
|
|
439
|
+
|
|
440
|
+
tempFilePath = os.path.join(tempOutputFolder, fileName)
|
|
441
|
+
response = requests.get(url = "{}/download/{}/{}".format(apiBaseUrl, finalResponseRequestId, fileName), headers={"Authorization": "Bearer APIKEY@{}".format(apiKey)})
|
|
442
|
+
with open(tempFilePath, 'wb') as f:
|
|
443
|
+
f.write(response.content)
|
|
444
|
+
|
|
445
|
+
fileReceived: FileReceivedObj = FileReceivedObj(fileName, tempFilePath)
|
|
446
|
+
outcomeItem['value'] = fileReceived
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
results.ComputeCost = finalResponseJson.get('computeCost')
|
|
451
|
+
results.OutcomeValues = outcomeValues
|
|
452
|
+
results.Outcomes = outcomes
|
|
453
|
+
results.RequestId = finalResponseRequestId
|
|
454
|
+
results.ContextId = finalResponseJson.get('contextId')
|
|
455
|
+
results.ErrorMessages = finalResponseJson.get('errorMessages')
|
|
456
|
+
|
|
457
|
+
except Exception as e:
|
|
458
|
+
print(e)
|
|
459
|
+
|
|
460
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .UnityPredictLocalHost import UnityPredictHost
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: unitypredict-engines
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Description-Content-Type: text/markdown
|
|
5
|
+
|
|
6
|
+
# UnityPredict Local App Engine Creator
|
|
7
|
+
|
|
8
|
+
## Introduction
|
|
9
|
+
|
|
10
|
+
The `UnityPredict` python sdk is designed to help accelerate the testing and debugging of **App Engines** for deployment on the `UnityPredict` platform.
|
|
11
|
+
|
|
12
|
+
On `UnityPredict`, **"Engines"** are the underlying compute framework that is executed, at scale, to perform inference or run business logic. In contrast, **"Models"** define the interface for these Engines. **Every Engine must be connected to a "Model"** because the Model serves as the interface that defines how `UnityPredict` communicates with the Engine. The Model specifies variable names and data types for inputs and outputs. Additionally, `UnityPredict` uses the Model definition to auto-generate APIs and user interfaces.
|
|
13
|
+
|
|
14
|
+
**"App Engines"** are specialized extensions of `UnityPredict` Engines that allow developers to write custom Python code, which the platform will execute at scale. These custom-defined Engines offer developers the flexibility needed to create complex applications. Within an App Engine, developers can access various platform features through code. For instance, **App Engine code can easily invoke other models (aka. chaining) or define cost calculations**. App Engines also enable developers to choose specific hardware types for running their code.
|
|
15
|
+
|
|
16
|
+
This guide focuses on the local development and testing of custom App Engine code.
|
|
17
|
+
|
|
18
|
+
For a full guide on how to use App Engine(s) on UnityPredict, please visit our complete help documentation here [UnityPredict Docs](https://console.unitypredict.com).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
* You can use pip to install the ```unitypredict``` library.
|
|
22
|
+
```bash
|
|
23
|
+
pip install unitypredict
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### EntryPoint.py
|
|
29
|
+
|
|
30
|
+
#### What is `EntryPoint.py`
|
|
31
|
+
|
|
32
|
+
`EntryPoint.py` is the script created by the **user** to provide the platform a well-defined function that can be invoked as the **starting point** for the **inference code**. It also acts as a gateway to access the **"platform"** object containing various features provided by UnityPredict
|
|
33
|
+
|
|
34
|
+
### Create an example `EntryPoint.py`
|
|
35
|
+
|
|
36
|
+
Create a custom entrypoint of your App Engine under the file **`EntryPoint.py`** containing the inference logic.
|
|
37
|
+
|
|
38
|
+
**NOTE:** The name of the file should be **`EntryPoint.py`**. If not followed, during the actual deployment on `UnityPredict` repository, the created file might not get invoked due to anincorrect name.
|
|
39
|
+
|
|
40
|
+
Given below is an example of a simple **`EntryPoint.py`**
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from unitypredict.Platform import IPlatform, InferenceRequest, InferenceResponse, OutcomeValue, InferenceContextData
|
|
45
|
+
import datetime
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_engine(request: InferenceRequest, platform: IPlatform) -> InferenceResponse:
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
platform.logMsg("Running User Code...")
|
|
54
|
+
response = InferenceResponse()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
prompt = request.InputValues['InputMessage']
|
|
58
|
+
|
|
59
|
+
currentExecTime = datetime.datetime.now()
|
|
60
|
+
currentExecTime = currentExecTime.strftime("%d-%m-%YT%H-%M-%S")
|
|
61
|
+
resp_message = "Echo message: {} Time:: {}".format(prompt, currentExecTime)
|
|
62
|
+
|
|
63
|
+
response.Outcomes['OutputMessage'] = [OutcomeValue(value=resp_message, probability=1.0)]
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
response.ErrorMessages = "Entrypoint Exception Occured: {}".format(str(e))
|
|
67
|
+
|
|
68
|
+
print("Finished Running User Code...")
|
|
69
|
+
return response
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Running the `EntryPoint.py`
|
|
74
|
+
|
|
75
|
+
In order to run the `EntryPoint.py` locally, add the following `main` section to the file itself.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
#### For Local testing ###
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
|
|
82
|
+
from unitypredict import UnityPredictHost
|
|
83
|
+
|
|
84
|
+
# Initialize the UnityPredict Platform
|
|
85
|
+
platform = UnityPredictHost()
|
|
86
|
+
|
|
87
|
+
testRequest = InferenceRequest()
|
|
88
|
+
testRequest.InputValues = {}
|
|
89
|
+
testRequest.InputValues["InputMessage"] = "Hi, this is an echo message"
|
|
90
|
+
results: InferenceResponse = run_engine(testRequest, platform)
|
|
91
|
+
|
|
92
|
+
# Print Outputs
|
|
93
|
+
if (results.Outcomes != None):
|
|
94
|
+
for outcomKeys, outcomeValues in results.Outcomes.items():
|
|
95
|
+
print ("\n\nOutcome Key: {}".format(outcomKeys))
|
|
96
|
+
for values in outcomeValues:
|
|
97
|
+
infVal: OutcomeValue = values
|
|
98
|
+
print ("Outcome Value: \n{}\n\n".format(infVal.Value))
|
|
99
|
+
print ("Outcome Probability: \n{}\n\n".format(infVal.Probability))
|
|
100
|
+
|
|
101
|
+
# Print Error Messages (if any)
|
|
102
|
+
print ("Error Messages: {}".format(results.ErrorMessages))
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Now run the `EntryPoint.py`
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python EntryPoint.py
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Output
|
|
112
|
+
|
|
113
|
+
The output should be the message added as the `InputMessage` in the main, appended with the timestamp of the execution.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
Config file detected, loading data from: D:\Documents\SelfProjects\UPTAzure\unitypredict-sdks\mainFolder\config.json
|
|
117
|
+
|
|
118
|
+
Running User Code...
|
|
119
|
+
|
|
120
|
+
Finished Running User Code...
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
Outcome Key: OutputMessage
|
|
124
|
+
Outcome Value:
|
|
125
|
+
Echo message: Hi, this is an echo message Time:: 18-08-2024T18-56-33
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
Outcome Probability:
|
|
129
|
+
1.0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
While running the script for the **first time**, you may also encounter a log message as follows.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
Config file not detected, creating templated config file: {project-current-folder}\config.json
|
|
137
|
+
```
|
|
138
|
+
This marks the auto-creation of the config file template `config.json`, along with the tree-structure following the templated config of the project which is the **mock building block** for the APIs of `UnityPredict` used under `EntryPoint.py`.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
### Config File and the project tree structure
|
|
143
|
+
|
|
144
|
+
The templated `config.json` looks like this:
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"ModelsDirPath": "{userPWD}/unitypredict_mocktool/models",
|
|
148
|
+
"RequestFilesDirPath": "{userPWD}/unitypredict_mocktool/requests",
|
|
149
|
+
"SAVE_CONTEXT": true,
|
|
150
|
+
"TempDirPath": "{userPWD}/unitypredict_mocktool/tmp",
|
|
151
|
+
"UPT_API_KEY": ""
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
And the tree structure of the generated directory will be:
|
|
156
|
+
```plaintext
|
|
157
|
+
{userPWD}/
|
|
158
|
+
├── unitypredict_mocktool/
|
|
159
|
+
| ├── models/
|
|
160
|
+
| ├── requests/
|
|
161
|
+
| └── tmp/
|
|
162
|
+
└── EntryPoint.py
|
|
163
|
+
└── config.json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Details of the JSON settings:
|
|
167
|
+
|
|
168
|
+
* **ModelsDirPath**: Local model files/binaries to be uploaded to AppEngine for using during the execution can be added under the specified **ModelsDirPath**
|
|
169
|
+
|
|
170
|
+
* **RequestFilesDirPath**: Files or Folders to be uploaded to AppEngine for using during the execution can be added under the specified **RequestFilesDirPath**
|
|
171
|
+
|
|
172
|
+
* **SAVE_CONTEXT**: Retains context across multiple requests. Disable it using ```"SAVE_CONTEXT" : false```
|
|
173
|
+
|
|
174
|
+
* **UPT_API_KEY**: API Key token generated from the UnityPredict profile of the user.
|
|
175
|
+
|
|
176
|
+
* **TempDirPath**: Temporary directory which can be used for writing or reading extra files or folders depending on the requirements of the App Engine
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
Copyright 2013 Mir Ikram Uddin
|
|
183
|
+
|
|
184
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
185
|
+
you may not use this file except in compliance with the License.
|
|
186
|
+
You may obtain a copy of the License at
|
|
187
|
+
|
|
188
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
189
|
+
|
|
190
|
+
Unless required by applicable law or agreed to in writing, software
|
|
191
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
192
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
193
|
+
See the License for the specific language governing permissions and
|
|
194
|
+
limitations under the License.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
unitypredict_engines/Models.py
|
|
4
|
+
unitypredict_engines/Platform.py
|
|
5
|
+
unitypredict_engines/UnityPredictLocalHost.py
|
|
6
|
+
unitypredict_engines/__init__.py
|
|
7
|
+
unitypredict_engines/scripts.py
|
|
8
|
+
unitypredict_engines.egg-info/PKG-INFO
|
|
9
|
+
unitypredict_engines.egg-info/SOURCES.txt
|
|
10
|
+
unitypredict_engines.egg-info/dependency_links.txt
|
|
11
|
+
unitypredict_engines.egg-info/entry_points.txt
|
|
12
|
+
unitypredict_engines.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
unitypredict_engines
|