├── .github └── PULL_REQUEST_TEMPLATE ├── .gitignore ├── .npmignore ├── AUTHORS.txt ├── CHANGES.txt ├── LICENSE ├── README.md ├── examples └── .gitsave ├── lib ├── custom.js ├── image.js ├── indico.js ├── pdf.js ├── services.js └── settings.js ├── package.json └── test ├── base64.txt ├── dog.jpg ├── face1.png ├── integration ├── custom.js ├── imagerecognition.js ├── images.js ├── imagetypes.js ├── pdfs.js ├── testkeywordsv2.js ├── testpolitical.js ├── testsummarization.js ├── text.js └── versioning.js ├── jpgbase64.txt ├── settings.js └── test.pdf /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 2 | Reviewers: 3 | ---------- 4 | 5 | - [ ] @mention 6 | 7 | What's the purpose of the PR? 8 | ----------------------------- 9 | 10 | 11 | 12 | 13 | Description of changes 14 | ---------------------- 15 | 16 | 17 | Notes to reviewers 18 | ------------------ 19 | 20 | 21 | 22 | 23 | Keep in mind while reviewing code: 24 | ---------------------------------- 25 | - Is relevant code tested? 26 | - Are added functions/methods documented? 27 | - Separation of concerns (SOC) 28 | - Don't repeat yourself (DRY) 29 | - Limit the number of positional arguments 30 | - Is function/method length reasonable? 31 | - Can code be broken down into smaller components? 32 | - Where do added functions/methods belong? 33 | - Are variable names descriptive? 34 | - Are errors handled appropriately? 35 | - Can logic be simplified? 36 | - Does the code make sense in context? (expand the diff) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Node-related 11 | node_modules 12 | node 13 | 14 | # Emacs 15 | *.*~ 16 | */*.*~ 17 | */#* 18 | */.#* 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directory 33 | # Deployed apps should consider commenting this line out: 34 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 35 | node_modules 36 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.csv 3 | Gruntfile.js -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | jeloou 2 | Slater-Victoroff 3 | JoseRoman 4 | MadisonMay 5 | aidankmcl 6 | sihrc 7 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1.3, Wed Aug 6 -- Added Changelog, also changed all instances of snake case to camelcase 2 | v0.1.4, Wed Aug 6 -- Updated README to reflect changes in the political API 3 | v0.1.5, Fri Sept 26 -- 4 | v0.1.6, Tue Oct 21 -- Updated to route calls to new remote server 5 | v0.1.7, Fri Nov 7 -- Updated to include new texttags API 6 | v0.1.9, Fri Nov 7 -- Readme update 7 | v0.1.10, Wed Nov 26 -- Batch support 8 | v0.1.11, Thu Dec 19 -- Batch authorization 9 | v0.2.0, Wed March 18 -- Private cloud support and auth configuration 10 | v0.3.1, Thu May 29 -- Log client library name and version number 11 | v0.4.0, Fri Jul 10 -- Facial localization, Named Entities SentimentHQ, and Keywords APIs added 12 | v0.4.1, Fri Jul 10 -- Added Twitter Engagement API, Batch functionality deprecation, resizing logic for better performance 13 | v0.4.2, Fri Aug 14 -- Added intersections Api, predictText -> analyzeText, predictImage -> analyzeImage 14 | v0.4.3, Tue Sept 22 -- Added Versioning and Image Recognition 15 | v0.4.4, Tue Sept 22 -- LWIP update 16 | v0.4.5, Tue Oct 14 -- Added URL Support 17 | v0.4.6 Fri Oct 30 -- Raise warning on credit limit exceeded 18 | v0.5.0 Web Dec 2 -- Addition of custom APIs, personality + persona API 19 | v0.5.1 Thu Dec 17 -- Addition of relevance, people, places, and organizations APIs 20 | v0.6.0 Fri Jan 22 -- Add text features API 21 | v0.6.1 Thu Feb 12 -- Update imagefeatures resizing 22 | v0.6.2 Fri Feb 19 -- Assorted custom collections UX improvements 23 | v0.7.0 Thu Mar 03 -- Rework of error handling logic, downgrade LWIP to optional dependency 24 | v0.8.0 Tue Mar 22 -- Addition of emotion API 25 | v0.9.0 Wed Mar 30 -- Pass API key in header instead of url param 26 | v0.9.1 Thu Mar 31 -- Change default settings for text features and relevance APIs 27 | v0.9.2 Wed Apr 6 -- Deprecate Named Entities Recognition 28 | v0.9.3 Wed Apr 27 -- More efficient query for model status 29 | v0.9.5 Tues Jun 14 -- Political API version 2 upgrade 30 | v0.9.6 Wed Jun 29 -- NER version 2 31 | v0.10.0 Thu Jul 7 -- Support for custom collection permissioning 32 | v0.10.1 Sat Aug 14 -- Hotfix for url protocols 33 | v0.10.3 Tue Sep 27 -- Add summarization API support 34 | v0.10.4 Tue Sep 27 -- FIX lwip dependency - 0.0.8 -> 0.0.9 35 | v0.10.5 Tue Oct 19 -- Add PDF Extraction support 36 | v0.10.6 Wed June 07 -- Add Custom Collections Explain 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 IndicoDataSolutions 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 | # indico.io 2 | 3 | A node.js wrapper for the [indico API](http://indico.io). 4 | 5 | ### Installation 6 | 7 | Install with [npm](http://npmjs.org/) 8 | 9 | ``` 10 | npm install indico.io 11 | ``` 12 | 13 | API Keys + Setup 14 | ---------------- 15 | For API key registration and setup, checkout our [quickstart guide](https://indico.io/docs#quickstart). 16 | 17 | ### Supported APIs 18 | 19 | #### Text 20 | - Sentiment Analysis 21 | - Text Tagging 22 | - Political Analysis 23 | - Keyword Detection 24 | - Summarization 25 | - Personality Detection 26 | - People, Place, and Organizations Detection 27 | - Text Relevance Analysis 28 | - Language Detection 29 | - Twitter Virality 30 | - Intersectional Analysis 31 | - Multi-API Analysis 32 | 33 | #### Image 34 | - Facial Emotion Recognition 35 | - Image Feature Extraction 36 | - Facial Feature Extraction 37 | - Face Localization 38 | - Content Filtering 39 | - Image Recognition 40 | - Multi-API Analysis 41 | 42 | #### Custom Collections 43 | - Train on your own data and make customized predictions 44 | 45 | Full Documentation 46 | ------------ 47 | Detailed documentation and further code examples are available at [indico.io/docs](https://indico.io/docs). 48 | 49 | 50 | ### Examples 51 | 52 | ```javascript 53 | var indico = require('indico.io'); 54 | 55 | // Be sure to set your API key 56 | indico.apiKey = "YOUR_API_KEY"; 57 | 58 | // Calls to the API return promises 59 | 60 | indico 61 | .political('Guns don\'t kill people. People kill people.') 62 | .then(function(res){ 63 | console.log(res); // { Libertarian: 0.47740164630834825, Liberal: 0.16617097211030055, Green: 0.08454409540443657, Conservative: 0.2718832861769146} 64 | }) 65 | .catch(function(err){ 66 | console.log('err: ', err); 67 | }) 68 | .then(indico.sentiment) 69 | 70 | indico 71 | .sentiment('Worst movie ever.') 72 | .then(function(){ 73 | console.log(res); // {Sentiment: 0.07062467665597527} 74 | }) 75 | .catch(function(err){ 76 | console.log('err: ', err); 77 | }) 78 | 79 | indico 80 | .sentiment('Really enjoyed the movie.') 81 | .then(function(){ 82 | console.log(res); // {Sentiment: 0.8105182526856075} 83 | }) 84 | .catch(function(err){ 85 | console.log('err: ', err); 86 | }) 87 | 88 | 89 | indico 90 | .language('Quis custodiet ipsos custodes') 91 | .then(function(){ 92 | console.log(res); // {u'Swedish': 0.00033330636691921914, u'Lithuanian': 0.007328693814717631, u'Vietnamese': 0.0002686116137658802, u'Romanian': 8.133913804076592e-06, u'Dutch': 0.09380619821813883, u'Korean': 0.00272046505489883, u'Danish': 0.0012556466207667206, u'Indonesian': 6.623391878530033e-07, u'Latin': 0.8230599921384231, u'Hungarian': 0.0012793617391960567, u'Persian (Farsi)': 0.0019848504383980473, u'Turkish': 0.0004606965429738638, u'French': 0.00016792646226101638, u'Norwegian': 0.0009179030069742254, u'Russian': 0.0002643396088456642, u'Thai': 7.746466749651003e-05, u'Finnish': 0.0026367338676522643, u'Spanish': 0.011844579596827902, u'Bulgarian': 3.746416283126873e-05, u'Greek': 0.027456554742563633, u'Tagalog': 0.0005143018200605518, u'English': 0.00013517846159760138, u'Esperanto': 0.0002599482830232367, u'Italian': 2.650711180999111e-06, u'Portuguese': 0.013193681336032896, u'Chinese': 0.008818957727120736, u'German': 0.00011732494215411359, u'Japanese': 0.0005885208894664065, u'Czech': 9.916434007248934e-05, u'Slovak': 8.869445598583308e-05, u'Hebrew': 3.70933525938127e-05, u'Polish': 9.900290296255447e-05, u'Arabic': 0.00013589586110619373} 93 | .texttags("This coconut green tea is amazing."); // {u'food': 0.3713687833244494, u'cars': 0.0037924017632370586, ...} 94 | }) 95 | .catch(function(err){ 96 | console.log('err: ', err); 97 | }) 98 | 99 | 100 | /* 101 | testImage is a b64 encoded image (PNG or JPG) 102 | */ 103 | 104 | indico 105 | .imageFeatures(testImage) 106 | .then(function(){ 107 | console.log(res); // [0.0, -0.02568680526917187, ... , 3.0342637531932777] 108 | }) 109 | .catch(function(err){ 110 | console.log('err: ', err); 111 | }) 112 | 113 | indico 114 | .fer(testImage) 115 | .then(function(){ 116 | console.log(res); // {Angry: 0.08843749137458341, Sad: 0.39091163159204684, Neutral: 0.1947947999669361, Surprise: 0.03443785859010413, Fear: 0.17574534848440568, Happy: 0.11567286999192382} 117 | }) 118 | .catch(function(err){ 119 | console.log('err: ', err); 120 | }); 121 | 122 | ``` 123 | 124 | ###Batch 125 | 126 | Batch requests allow you to process larger volumes of data more efficiently by grouping many examples into a single request. Simply call the batch method that corresponds to the API you'd like to use, and ensure your data is wrapped in an array. 127 | 128 | ```javascript 129 | var indico = require('indico.io') 130 | 131 | indico 132 | .batchSentiment(['Worst movie ever.', 'Best movie ever.']) 133 | .then(function(res){ 134 | console.log(res) // [ 0.07808824238341827, 0.813400530597089 ] 135 | }) 136 | .catch(function(err){ 137 | console.log('err: ', err); 138 | }) 139 | 140 | ``` 141 | -------------------------------------------------------------------------------- /examples/.gitsave: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndicoDataSolutions/IndicoIo-node/80f86c95fe02480b9dedc6935be9477f0c0ea6a4/examples/.gitsave -------------------------------------------------------------------------------- /lib/custom.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | , makeRequest = require('./services.js').makeRequest 3 | , addKeywordArguments = require('./services.js').addKeywordArguments; 4 | 5 | var Collection = function(collection, config) { 6 | var _this = this; 7 | this.promise_chain = Promise.resolve(); 8 | 9 | this.collection = collection; 10 | config = config || {}; 11 | config['collection'] = collection; 12 | this.version = config["version"] || 1; 13 | this.baseConfig = config; 14 | 15 | this.addData = function(data, config) { 16 | var config = addKeywordArguments(config || {}, _this.baseConfig); 17 | _this.promise_chain = _this.promise_chain.then(function() { 18 | var deferred = Promise.pending(); 19 | 20 | if (typeof data === 'undefined') { 21 | var msg = "Must submit data in format ['data', 'label']. You can also submit multiple in a list."; 22 | deferred.reject(new Error(msg)); 23 | } 24 | 25 | var batch = typeof data !== 'undefined' && typeof data[0] !== 'string'; 26 | makeRequest('/custom/add_data', this.version, data, batch, config).then(function (res) { 27 | deferred.resolve(res); 28 | }); 29 | return deferred.promise; 30 | }); 31 | return _this; 32 | }; 33 | 34 | this.removeExample = function(data, config) { 35 | var config = addKeywordArguments(config || {}, _this.baseConfig); 36 | _this.promise_chain = _this.promise_chain.then(function() { 37 | var deferred = Promise.pending(); 38 | if (typeof data === 'undefined') { 39 | var msg = "Must submit examples for removal in string format (i.e. 'example'). You can also submit multiple in a list."; 40 | deferred.reject(new Error(msg)); 41 | } 42 | var batch = (typeof data !== 'undefined' && typeof data !== 'string') ? true : false; 43 | 44 | makeRequest('/custom/remove_example', this.version, data, batch, config).then(function(res) { 45 | deferred.resolve(res); 46 | }); 47 | return deferred.promise; 48 | }); 49 | return _this; 50 | } 51 | 52 | this.train = function(config) { 53 | var config = addKeywordArguments(config || {}, _this.baseConfig); 54 | _this.promise_chain = _this.promise_chain.then(function() { 55 | var deferred = Promise.pending(); 56 | makeRequest('/custom/train', this.version, {}, false, config).then(function(res) { 57 | deferred.resolve(res); 58 | }); 59 | return deferred.promise; 60 | }); 61 | return _this; 62 | }; 63 | 64 | this.wait = function(status, config) { 65 | var config = addKeywordArguments(config || {}, _this.baseConfig); 66 | _this.promise_chain = _this.promise_chain.then(function() { 67 | var deferred = Promise.pending(); 68 | var waitForTrained = setInterval(function() { 69 | makeRequest('/custom/info', this.version, {}, false, config).then(function(collection) { 70 | if (!collection) { 71 | var msg = _this.collection + " does not exist at the moment! Make sure this is something that has been created"; 72 | deferred.reject(new Error(msg)); 73 | } 74 | if (collection && collection['status'] !== 'training') { 75 | if (collection["status"] !== "ready") { 76 | var msg = _this.collection + " failed with status " + collection["status"]; 77 | deferred.reject(new Error(msg)); 78 | } 79 | clearInterval(waitForTrained); 80 | deferred.resolve(collection); 81 | } 82 | }); 83 | }, 1000); 84 | return deferred.promise; 85 | }); 86 | return _this; 87 | }; 88 | 89 | this.info = function(config) { 90 | var config = addKeywordArguments(config || {}, _this.baseConfig); 91 | _this.promise_chain = _this.promise_chain.then(function() { 92 | var deferred = Promise.pending(); 93 | makeRequest('/custom/info', this.version, {}, false, config).then(function(collection) { 94 | if (collection['status'] === undefined) { 95 | deferred.resolve({ 96 | model_type: null, 97 | input_type: null, 98 | number_of_examples: 0, 99 | status: 'no examples' 100 | }); 101 | } else { 102 | deferred.resolve(collection); 103 | } 104 | }) 105 | return deferred.promise; 106 | }); 107 | return _this; 108 | } 109 | 110 | this.predict = function(data, config) { 111 | var config = addKeywordArguments(config || {}, _this.baseConfig); 112 | _this.promise_chain = _this.promise_chain.then(function() { 113 | var deferred = Promise.pending(); 114 | if (typeof data === 'undefined') { 115 | var msg = "Must submit data in string format 'data'. You can also submit multiple in a list."; 116 | deferred.reject(new Error(msg)); 117 | } 118 | 119 | var batch = (typeof data !== 'undefined' && typeof data !== 'string') ? true : false; 120 | 121 | makeRequest('/custom/predict', this.version, data, batch, config).then(function(res) { 122 | deferred.resolve(res); 123 | }); 124 | return deferred.promise; 125 | }); 126 | return _this; 127 | }; 128 | 129 | this.explain = function(data, config) { 130 | var config = addKeywordArguments(config || {}, _this.baseConfig); 131 | _this.promise_chain = _this.promise_chain.then(function() { 132 | var deferred = Promise.pending(); 133 | if (typeof data === 'undefined') { 134 | var msg = "Must submit data in string format 'data'. You can also submit multiple in a list."; 135 | deferred.reject(new Error(msg)); 136 | } 137 | 138 | var batch = (typeof data !== 'undefined' && typeof data !== 'string') ? true : false; 139 | 140 | makeRequest('/custom/explain', this.version, data, batch, config).then(function(res) { 141 | deferred.resolve(res); 142 | }); 143 | return deferred.promise; 144 | }); 145 | return _this; 146 | }; 147 | 148 | this.clear = function(config) { 149 | var config = addKeywordArguments(config || {}, _this.baseConfig); 150 | _this.promise_chain = _this.promise_chain.then(function() { 151 | var deferred = Promise.pending(); 152 | makeRequest('/custom/clear_collection', this.version, {}, false, config).then(function(res) { 153 | deferred.resolve(res); 154 | }); 155 | return deferred.promise; 156 | }); 157 | return _this; 158 | } 159 | 160 | this.rename = function(name, config) { 161 | var config = addKeywordArguments(config || {}, _this.baseConfig); 162 | config['name'] = name; 163 | _this.promise_chain = _this.promise_chain.then(function() { 164 | var deferred = Promise.pending(); 165 | 166 | 167 | makeRequest('/custom/rename', this.version, {}, false, config).then(function(res) { 168 | _this.baseConfig['collection'] = name; 169 | _this.collection = name; 170 | deferred.resolve(res); 171 | }); 172 | return deferred.promise; 173 | }); 174 | return _this; 175 | } 176 | 177 | this.register = function(config) { 178 | var config = addKeywordArguments(config || {}, _this.baseConfig); 179 | _this.promise_chain = _this.promise_chain.then(function() { 180 | var deferred = Promise.pending(); 181 | makeRequest('/custom/register', this.version, {}, false, config).then(function(res) { 182 | deferred.resolve(res); 183 | }); 184 | return deferred.promise; 185 | }); 186 | return _this; 187 | } 188 | 189 | this.deregister = function(config) { 190 | var config = addKeywordArguments(config || {}, _this.baseConfig); 191 | _this.promise_chain = _this.promise_chain.then(function() { 192 | var deferred = Promise.pending(); 193 | 194 | makeRequest('/custom/deregister', this.version, {}, false, config).then(function(res) { 195 | deferred.resolve(res); 196 | }); 197 | return deferred.promise; 198 | }); 199 | return _this; 200 | } 201 | 202 | this.authorize = function(email, config) { 203 | var config = addKeywordArguments(config || {}, _this.baseConfig); 204 | config['email'] = email; 205 | if (config['permission_type'] === undefined) { 206 | config['permission_type'] = 'read'; 207 | } 208 | _this.promise_chain = _this.promise_chain.then(function() { 209 | var deferred = Promise.pending(); 210 | 211 | makeRequest('/custom/authorize', this.version, {}, false, config).then(function(res) { 212 | deferred.resolve(res); 213 | }); 214 | return deferred.promise; 215 | }); 216 | return _this; 217 | } 218 | 219 | this.deauthorize = function(email, config) { 220 | var config = addKeywordArguments(config || {}, _this.baseConfig); 221 | config['email'] = email; 222 | _this.promise_chain = _this.promise_chain.then(function() { 223 | var deferred = Promise.pending(); 224 | 225 | makeRequest('/custom/deauthorize', this.version, {}, false, config).then(function(res) { 226 | deferred.resolve(res); 227 | }); 228 | return deferred.promise; 229 | }); 230 | return _this; 231 | } 232 | 233 | this.then = function(callback) { 234 | return this.promise_chain.then(callback); 235 | } 236 | 237 | this.catch = function(errback) { 238 | return this.promise_chain.catch(errback); 239 | } 240 | 241 | return this; 242 | } 243 | 244 | module.exports = Collection 245 | -------------------------------------------------------------------------------- /lib/image.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | filetype = require("file-type"), 3 | request = require('request'), 4 | validUrl = require("valid-url"), 5 | Promise = require('bluebird'); 6 | 7 | try { 8 | lwip = require('lwip') 9 | } catch (err) { 10 | lwip = null; 11 | } 12 | 13 | // Backwards compatibility for array checking 14 | if (typeof Array.isArray === 'undefined') { 15 | Array.isArray = function (obj) { 16 | return Object.prototype.toString.call(obj) === '[object Array]'; 17 | } 18 | }; 19 | 20 | var handleString = function (image) { 21 | return new Promise(function (resolve, reject) { 22 | var input, options; 23 | if (validUrl.isWebUri(image)) { 24 | resolve({ 25 | "image": image, 26 | "type": "url" 27 | }); 28 | } else if (typeof image === 'string') { 29 | if (image.length <= 260 && fs.lstatSync(image).isFile()) { 30 | input = image; 31 | var arr = image.split('.'); 32 | options = arr[arr.length - 1]; 33 | resolve({ 34 | "image": input, 35 | "options": options, 36 | }); 37 | } else { 38 | input = new Buffer(image, 'base64'); 39 | options = filetype(input).ext; 40 | resolve({ 41 | "image": input, 42 | "options": options, 43 | }); 44 | } 45 | } else { 46 | resolve(new Error( 47 | "Data input type must be array, filepath, or base64 encoded string" 48 | )); 49 | } 50 | }); 51 | } 52 | var handleImage = function (image, size, min_axis) { 53 | return new Promise(function (resolve, reject) { 54 | handleString(image).then(function (output) { 55 | var input = output.image; 56 | var options = output.options; 57 | var type = output.type; 58 | 59 | if (type === "url") { 60 | resolve(input); 61 | } else { 62 | // Image reading and resizing 63 | if (!lwip) { 64 | reject(new Error( 65 | "Image processing dependency LWIP could not be loaded." 66 | )) 67 | } 68 | try { 69 | lwip.open(input, options, function (err, image) { 70 | var ratio = image.width() / image.height(); 71 | if (ratio >= 10 || ratio <= .1) 72 | console.warn( 73 | "For best performance, we recommend using images of aspect ratio less than 1:10." 74 | ); 75 | 76 | if (size) { 77 | var new_height = size, 78 | new_width = size; 79 | 80 | if (min_axis) { 81 | new_height = ratio > 1 ? 1 / ratio * size : size; 82 | new_width = ratio > 1 ? size : ratio * size; 83 | } 84 | image.resize(new_width, new_height, function (err, image) { 85 | image.toBuffer('png', function (err, buffer) { 86 | resolve(buffer.toString("base64")); 87 | }); 88 | }); 89 | } else { 90 | image.toBuffer('png', function (err, buffer) { 91 | resolve(buffer.toString("base64")); 92 | }); 93 | } 94 | }); 95 | } catch (err) { 96 | reject(new Error("An unknown error while loading your image.")); 97 | } 98 | } 99 | }); 100 | }); 101 | } 102 | 103 | module.exports = { 104 | preprocess: function (images, size, min_axis, batch) { 105 | return new Promise(function (resolve, reject) { 106 | // aggregate asynchronous results for batch reqs 107 | if (batch) { 108 | Promise.all(images.map(function (image) { 109 | return handleImage(image, size, min_axis); 110 | })).then(function (data) { 111 | resolve(data); 112 | }); 113 | } else { 114 | handleImage(images, size).then(function (b64) { 115 | resolve(b64); 116 | }); 117 | } 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/indico.js: -------------------------------------------------------------------------------- 1 | var services = require('./services.js').services 2 | , makeRequest = require('./services.js').makeRequest 3 | , apiRequest = require('./services.js').apiRequest 4 | , detectBatch = require('./services.js').detectBatch 5 | , Collection = require('./custom.js') 6 | , indico = require('./services.js').base; 7 | 8 | function capitalize(str) { 9 | return str.charAt(0).toUpperCase() + str.slice(1); 10 | } 11 | 12 | function snakeCase(str) { 13 | return str.replace(/([A-Z])/g, function (char) { 14 | return '_' + char.toLowerCase(); 15 | }); 16 | } 17 | 18 | services.forEach(function (api) { 19 | indico[api.name] = apiRequest(api, false); 20 | indico["batch" + capitalize(api.name)] = apiRequest(api, true); 21 | }); 22 | 23 | // camelCase + snake_case + lowercase supported 24 | for (name in indico) { 25 | indico[snakeCase(name)] = indico[name]; 26 | indico[name.toLowerCase()] = indico[name]; 27 | } 28 | 29 | indico.Collection = function(collectionName, config) { 30 | return new Collection(collectionName, config); 31 | } 32 | 33 | indico.collections = function(config) { 34 | version = config['version'] || '1' 35 | delete config['version'] 36 | return makeRequest('/custom/collections', version, {}, false, config); 37 | } 38 | 39 | indico.relevance = function(data, queries, config) { 40 | var batch = detectBatch(data); 41 | config = config || {}; 42 | config.queries = queries 43 | version = config['version'] || '1' 44 | config.synonyms = config.synonyms || false; 45 | delete config['version'] 46 | return makeRequest('/relevance', version, data, batch, config) 47 | } 48 | indico.posneg = indico.sentiment; 49 | 50 | module.exports = indico; 51 | -------------------------------------------------------------------------------- /lib/pdf.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | filetype = require("file-type"), 3 | request = require('request'), 4 | validUrl = require("valid-url"), 5 | Promise = require('bluebird'); 6 | 7 | // Backwards compatibility for array checking 8 | if (typeof Array.isArray === 'undefined') { 9 | Array.isArray = function (obj) { 10 | return Object.prototype.toString.call(obj) === '[object Array]'; 11 | } 12 | }; 13 | 14 | var handleString = function (pdf) { 15 | return new Promise(function (resolve, reject) { 16 | var input, options; 17 | if (validUrl.isWebUri(pdf)) { 18 | // File 19 | resolve({ 20 | "pdf": pdf, 21 | "type": "url" 22 | }); 23 | } else if (typeof pdf === 'string') { 24 | if (pdf.length <= 260 && fs.lstatSync(pdf).isFile()) { 25 | // File 26 | input = pdf; 27 | resolve({ 28 | "pdf": input, 29 | "type": "file" 30 | }); 31 | } else { 32 | // Base64 encoded pdf 33 | input = new Buffer(pdf, 'base64'); 34 | resolve({ 35 | "pdf": input, 36 | "type": "b64" 37 | }); 38 | } 39 | } else { 40 | resolve(new Error( 41 | "Data input type must be url, filepath, or base64 encoded string" 42 | )); 43 | } 44 | }); 45 | } 46 | var handlePdf = function (pdf) { 47 | return new Promise(function (resolve, reject) { 48 | handleString(pdf).then(function (output) { 49 | var input = output.pdf; 50 | var type = output.type; 51 | 52 | if (type === "url" || type === 'b64') { 53 | resolve(input); 54 | } else { 55 | // Type: file 56 | try { 57 | fs.readFile(input, function (err, pdf) { 58 | resolve(pdf.toString('base64')); 59 | }); 60 | } catch (err) { 61 | reject(new Error("An unknown error while loading your pdf.")); 62 | } 63 | } 64 | }); 65 | }); 66 | } 67 | 68 | module.exports = { 69 | preprocess: function (pdfs, batch) { 70 | return new Promise(function (resolve, reject) { 71 | // aggregate asynchronous results for batch reqs 72 | if (batch) { 73 | Promise.all(pdfs.map(function (pdf) { 74 | return handlePdf(pdf); 75 | })).then(function (data) { 76 | resolve(data); 77 | }); 78 | } else { 79 | handlePdf(pdfs).then(function (b64) { 80 | resolve(b64); 81 | }); 82 | } 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/services.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | , settings = require('./settings.js') 3 | , image = require('./image.js') 4 | , pdf = require('./pdf.js') 5 | , request = Promise.promisify(require('request')); 6 | 7 | Promise.promisifyAll(request); 8 | 9 | var base = {}; 10 | base.apiKey = false; 11 | base.cloud = false; 12 | base.host = 'apiv2.indico.io'; 13 | 14 | function detectBatch(data) { 15 | return Object.prototype.toString.call(data).indexOf("Array") > -1; 16 | } 17 | 18 | function endsWith(str, suffix) { 19 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 20 | } 21 | 22 | function extend(target) { 23 | var sources = [].slice.call(arguments, 1); 24 | sources.forEach(function (source) { 25 | for (var prop in source) { 26 | target[prop] = source[prop]; 27 | } 28 | }); 29 | return target; 30 | } 31 | 32 | function service(name, privateCloud) { 33 | var url_protocol = "https"; 34 | 35 | /* given a set of parameters, returns the proper url for the REST api*/ 36 | if (privateCloud) { 37 | var url = privateCloud + '.indico.domains' + name; 38 | } else { 39 | var url = base.host + name; 40 | } 41 | 42 | if (!endsWith(base.host, "indico.io") && !endsWith(base.host, "indico.domains")) { 43 | url_protocol = "http" 44 | } 45 | 46 | return url_protocol + "://" + url; 47 | } 48 | 49 | function addKeywordArguments(body, config) { 50 | // pass along additional keyword args in JSON body 51 | for (var key in config) { 52 | if (key !== 'apiKey' && key !== 'privateCloud' && key !== 'apis') { 53 | body[key] = config[key]; 54 | } 55 | } 56 | return body; 57 | } 58 | 59 | function postprocess(results, name) { 60 | if (name.indexOf('apis') === 1) { 61 | var formatted_results = {}; 62 | for (api in results) { 63 | if ('results' in results[api]) { 64 | formatted_results[api] = results[api]['results']; 65 | } else { 66 | formatted_results[api] = results[api]; 67 | } 68 | } 69 | return formatted_results; } 70 | return results; 71 | } 72 | 73 | var makeRequest = function (name, version, data, batch, config) { 74 | var apiKey = settings.resolveApiKey(config, base.apiKey); 75 | var privateCloud = settings.resolvePrivateCloud(config, base.cloud); 76 | 77 | // Params passed in as keyword arguments 78 | var apis = config ? config['apis'] : false; 79 | var body = {}; 80 | 81 | body['data'] = data; 82 | body = addKeywordArguments(body, config); 83 | 84 | if (name.indexOf('custom') > -1 && batch) { 85 | name = '/custom/batch/' + name.split('custom/')[1]; 86 | } else { 87 | name = (batch ? name + "/batch" : name); 88 | } 89 | 90 | urlParams = {}; 91 | if (apis) { 92 | urlParams["apis"] = apis.join(","); 93 | } 94 | 95 | if (version) { 96 | urlParams["version"] = version; 97 | } 98 | 99 | var options = { 100 | method: 'POST', 101 | url: service(name, privateCloud), 102 | body: JSON.stringify(body), 103 | headers: { 104 | 'Content-type': 'application/json', 105 | 'Accept': 'text/plain', 106 | 'client-lib': 'node', 107 | 'version-number': '0.9.0', 108 | 'X-ApiKey': apiKey 109 | }, 110 | qs: urlParams 111 | }; 112 | 113 | return request(options).then(function (results) { 114 | var res = results[0]; 115 | var body = results[1]; 116 | /* 117 | TODO: Make sure this matches the API headers 118 | */ 119 | var warning = res.headers['X-Warning']; 120 | 121 | if (warning) { 122 | console.warn(warning); 123 | } 124 | if (res.statusCode !== 200) { 125 | return new Error(JSON.parse(body).error) 126 | } 127 | results = JSON.parse(body).results; 128 | results = postprocess(results, name); 129 | 130 | return results 131 | }); 132 | } 133 | 134 | var apiRequest = function (api, batch) { 135 | return function (data, config) { 136 | config = config || {} 137 | 138 | // use api defaults when present 139 | config = extend(config, api.config || {}); 140 | 141 | var batch = batch || detectBatch(data); 142 | var version = config.version || api.version; 143 | 144 | // Keywords Multilingual must be version 1 145 | if ("language" in config && config["language"] != "english") { 146 | version = 1; 147 | } 148 | 149 | if (api.type === "image") { 150 | var size = (api.name === "fer" && config["detect"]) ? false : api.size; 151 | return image.preprocess(data, size, api.min_axis, batch).then(function(packaged) { 152 | return makeRequest(api.endpoint, version, packaged, batch, config); 153 | }); 154 | } else if (api.type === "pdf") { 155 | return pdf.preprocess(data, batch).then(function(packaged) { 156 | return makeRequest(api.endpoint, version, packaged, batch, config); 157 | }); 158 | } else { 159 | return makeRequest(api.endpoint, version, data, batch, config); 160 | } 161 | }; 162 | } 163 | 164 | module.exports = { 165 | 'base': base, 166 | 'service': service, 167 | 'detectBatch': detectBatch, 168 | 'makeRequest': makeRequest, 169 | 'addKeywordArguments': addKeywordArguments, 170 | 'apiRequest': apiRequest, 171 | 'services': [ 172 | { 173 | name: 'intersections' 174 | , type: 'text' 175 | , endpoint: '/apis/intersections' 176 | }, 177 | { 178 | name: 'twitterEngagement' 179 | , type: 'text' 180 | , endpoint: '/twitterengagement' 181 | }, 182 | { 183 | name: 'political' 184 | , type: 'text' 185 | , endpoint: '/political' 186 | , version: 2 187 | }, 188 | { 189 | name: 'sentiment' 190 | , type: 'text' 191 | , endpoint: '/sentiment' 192 | }, 193 | { 194 | name: 'sentimentHQ' 195 | , type: 'text' 196 | , endpoint: '/sentimenthq' 197 | }, 198 | { 199 | name: 'personality' 200 | , type: 'text' 201 | , endpoint: '/personality' 202 | }, 203 | { 204 | name: 'personas' 205 | , type: 'text' 206 | , endpoint: '/personality' 207 | , config: {'persona': true} 208 | }, 209 | { 210 | name: 'language' 211 | , type: 'text' 212 | , endpoint: '/language' 213 | }, 214 | { 215 | name: 'textTags' 216 | , type: 'text' 217 | , endpoint: '/texttags' 218 | }, 219 | { 220 | name: 'keywords' 221 | , type: 'text' 222 | , endpoint: '/keywords' 223 | , version: 2 224 | }, 225 | { 226 | name: 'people' 227 | , type: 'text' 228 | , endpoint: '/people' 229 | , version: 2 230 | }, 231 | { 232 | name: 'places' 233 | , type: 'text' 234 | , endpoint: '/places' 235 | , version: 2 236 | }, 237 | { 238 | name: 'organizations' 239 | , type: 'text' 240 | , endpoint: '/organizations' 241 | , version: 2 242 | }, 243 | { 244 | name: 'namedEntities' 245 | , type: 'text' 246 | , endpoint: '/namedentities' 247 | , version: 2 248 | }, 249 | { 250 | name: 'textFeatures' 251 | , type: 'text' 252 | , endpoint: '/textfeatures' 253 | , synonyms: false 254 | }, 255 | { 256 | name: 'emotion' 257 | , type: 'text' 258 | , endpoint: '/emotion' 259 | }, 260 | { 261 | name: 'analyzeText' 262 | , type: 'text' 263 | , endpoint: '/apis/multiapi' 264 | }, 265 | { 266 | name: 'facialLocalization' 267 | , type: 'image' 268 | , size: false 269 | , endpoint: '/faciallocalization' 270 | }, 271 | { 272 | name: 'facialFeatures' 273 | , type: 'image' 274 | , size: 48 275 | , endpoint: '/facialfeatures' 276 | }, 277 | { 278 | name: 'fer' 279 | , type: 'image' 280 | , size: 48 281 | , endpoint: '/fer' 282 | }, 283 | { 284 | name: 'imageFeatures' 285 | , type: 'image' 286 | , size: 512 287 | , min_axis: true 288 | , endpoint: '/imagefeatures' 289 | , version: 3 290 | }, 291 | { 292 | name: 'imageRecognition' 293 | , type: 'image' 294 | , size: 144 295 | , min_axis: true 296 | , endpoint: '/imagerecognition' 297 | }, 298 | { 299 | name: 'contentFiltering' 300 | , type: 'image' 301 | , size: 128 302 | , min_axis: true 303 | , endpoint: '/contentfiltering' 304 | }, 305 | { 306 | name: 'analyzeImage' 307 | , type: 'image' 308 | , size: 64 309 | , endpoint: '/apis/multiapi' 310 | }, 311 | { 312 | name: 'summarization' 313 | , type: 'text' 314 | , endpoint: '/summarization' 315 | }, 316 | { 317 | name: 'pdfExtraction' 318 | , type: 'pdf' 319 | , endpoint: '/pdfextraction' 320 | } 321 | ] 322 | }; 323 | -------------------------------------------------------------------------------- /lib/settings.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird') 2 | , path = require('path') 3 | , fs = Promise.promisifyAll(require('fs')) 4 | , ini = Promise.promisifyAll(require('config-ini')) 5 | , expandTilde = require('expand-tilde') 6 | , settings = module.exports; 7 | 8 | var loadIndicorc = function() { 9 | return new Promise(function(resolve, reject){ 10 | /* 11 | Search for valid indicorc files to use as configuration 12 | Load files in order of precendence if any are found 13 | */ 14 | 15 | var localPath = path.resolve(process.cwd(), './.indicorc'); 16 | var globalPath = expandTilde('~/.indicorc'); 17 | var paths = [globalPath, localPath]; 18 | var validPaths = []; 19 | 20 | for (var i = path.length - 1; i >= 0; i--) { 21 | if (fs.existsSync(paths[i])) { 22 | validPaths.push(paths[i]); 23 | } 24 | } 25 | 26 | if (paths.length < 1) { 27 | reject(new Error('Could not find .indicorc and no local path was given')) 28 | } 29 | 30 | resolve(validPaths); 31 | }) 32 | .then(ini.load); 33 | } 34 | 35 | var indicorc = loadIndicorc().then(function(indicorcFile) { 36 | return indicorcFile; 37 | }) 38 | 39 | var resolveApiKey = function(config, moduleConfig, configFile) { 40 | /* 41 | Check whether auth credentials are provided via: 42 | - config: object passed in when function is called 43 | - moduleConfig: a constant defined in code after module import 44 | - environment variables: found in system environment 45 | - configFile: parsed from file at module import 46 | 47 | The last argument (configFile) is optional and is primarily 48 | provided to make testing simpler. 49 | */ 50 | configFile = configFile || indicorc; 51 | 52 | var validConfig = Boolean( 53 | config !== undefined && 54 | config !== null && 55 | config.hasOwnProperty('apiKey') 56 | ); 57 | 58 | var validEnvironmentVariables = Boolean( 59 | process.env.INDICO_API_KEY 60 | ); 61 | 62 | var validConfigFile = Boolean( 63 | configFile !== undefined && 64 | configFile.hasOwnProperty('auth') && 65 | configFile.auth.hasOwnProperty('api_key') 66 | ); 67 | if (validConfig) { 68 | return config.apiKey 69 | } else if (moduleConfig) { 70 | return moduleConfig; 71 | } else if (validEnvironmentVariables) { 72 | return process.env.INDICO_API_KEY 73 | } else if (validConfigFile) { 74 | return configFile.auth.api_key 75 | } else { 76 | return false; 77 | } 78 | } 79 | 80 | var resolvePrivateCloud = function(config, moduleConfig, configFile) { 81 | /* 82 | Check whether private cloud endpoints are provided via: 83 | - config: object passed in when function is called 84 | - moduleConfig: a constant defined in code after module import 85 | - environment variables: found in system environment 86 | - configFile: parsed from file at module import 87 | 88 | The second argument (configFile) is optional and is primarily 89 | provided to make testing simpler. 90 | */ 91 | 92 | configFile = configFile || indicorc; 93 | 94 | var validConfig = Boolean( 95 | config !== undefined && 96 | config.hasOwnProperty('privateCloud') 97 | ); 98 | 99 | var validEnvironmentVariables = Boolean( 100 | process.env.INDICO_CLOUD 101 | ); 102 | 103 | var validConfigFile = Boolean( 104 | configFile !== undefined && 105 | configFile.hasOwnProperty('private_cloud') && 106 | configFile.private_cloud.hasOwnProperty('cloud') 107 | ); 108 | 109 | if (validConfig) { 110 | return config.privateCloud; 111 | } else if (moduleConfig) { 112 | return moduleConfig; 113 | } else if (validEnvironmentVariables) { 114 | return process.env.INDICO_CLOUD; 115 | } else if (validConfigFile) { 116 | return configFile.private_cloud.cloud; 117 | } else { 118 | return false; 119 | } 120 | } 121 | 122 | settings.resolveApiKey = resolveApiKey; 123 | settings.resolvePrivateCloud = resolvePrivateCloud; 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "indico.io", 3 | "description": "A Node.js wrapper for the Indico’s API", 4 | "version": "0.10.6", 5 | "homepage": "https://github.com/IndicoDataSolutions/IndicoIo-node", 6 | "author": { 7 | "name": "Joseph Núñez ", 8 | "email": "toctochello@gmail.com" 9 | }, 10 | "contributors": [ 11 | "Madison May ", 12 | "Chris Lee ", 13 | "Aidan McLaughlin b['confidence']) { 12 | return -1; 13 | } 14 | // a must be equal to b 15 | return 0; 16 | } 17 | 18 | describe('Text', function() { 19 | this.timeout(10000); 20 | 21 | if (settings.resolveApiKey() === false) { 22 | // skip test -- indico auth keys are not available 23 | console.warn('Api keys are now required. Skipping some tests.\nhttp://indico.io/docs') 24 | return; 25 | } 26 | 27 | describe('emotion', function() { 28 | it('should get the right response format', function(done) { 29 | 30 | indico.emotion("I did it. I got into Grad School. Not just any program, but a GREAT program. :-)") 31 | .then(function(res) { 32 | Object.keys(res).should.have.length(5); 33 | done(); 34 | }) 35 | .catch(function(err){ 36 | done(err); 37 | return; 38 | }); 39 | }); 40 | }); 41 | 42 | describe('personality', function() { 43 | it('should get the right response format', function(done) { 44 | 45 | indico.personality("I love my friends!") 46 | .then(function(res) { 47 | Object.keys(res).should.have.length(4); 48 | done(); 49 | }) 50 | .catch(function(err){ 51 | done(err); 52 | return; 53 | }); 54 | }); 55 | }); 56 | 57 | describe('personas', function() { 58 | it('should get the right response format', function(done) { 59 | 60 | indico.personas("I love my friends!") 61 | .then(function(res) { 62 | Object.keys(res).should.have.length(16); 63 | done(); 64 | }) 65 | .catch(function(err){ 66 | done(err); 67 | return; 68 | }); 69 | }); 70 | }); 71 | 72 | describe('people', function() { 73 | it('should get the right response format', function(done) { 74 | 75 | var text = 'Barack Obama is scheduled to give a talk next Saturday at the White House.'; 76 | indico.people(text) 77 | .then(function(res) { 78 | res.sort(compare_by_confidence) 79 | res[0]['text'].should.contain('Barack Obama') 80 | indico.people(text, {"version": 1}) 81 | .then(function(resv1) { 82 | resv1.sort(compare_by_confidence) 83 | expect(Math.abs(res[0]['confidence'] - resv1[0]['confidence']) > .00001) 84 | done(); 85 | }) 86 | .catch(function(err){ 87 | done(err); 88 | return; 89 | }); 90 | }) 91 | .catch(function(err){ 92 | done(err); 93 | return; 94 | }); 95 | }); 96 | }) 97 | 98 | describe('people', function() { 99 | it('should get the right response format', function(done) { 100 | 101 | var text = 'Barack Obama is scheduled to give a talk next Saturday at the White House.'; 102 | indico.people([text, text]) 103 | .then(function(res) { 104 | res[0].sort(compare_by_confidence) 105 | res[0][0]['text'].should.contain('Barack Obama') 106 | indico.people([text, text], {"version": 1}) 107 | .then(function(resv1) { 108 | resv1.sort(compare_by_confidence) 109 | expect(Math.abs(res[0][0]['confidence'] - resv1[0][0]['confidence']) > .00001) 110 | done(); 111 | }) 112 | .catch(function(err){ 113 | done(err); 114 | return; 115 | }); 116 | }) 117 | .catch(function(err){ 118 | done(err); 119 | return; 120 | }); 121 | }); 122 | }) 123 | 124 | describe('places', function() { 125 | it('should get the right response format', function(done) { 126 | 127 | var text = 'Lets all go to Virginia beach before it gets too cold to wander outside.'; 128 | indico.places(text) 129 | .then(function(res) { 130 | res.sort(compare_by_confidence) 131 | res[0]['text'].should.contain('Virginia') 132 | indico.places(text, {"version": 1}) 133 | .then(function(resv1) { 134 | resv1.sort(compare_by_confidence) 135 | expect(Math.abs(res[0]['confidence'] - resv1[0]['confidence']) > .00001) 136 | done(); 137 | }) 138 | .catch(function(err){ 139 | done(err); 140 | return; 141 | }); 142 | }) 143 | .catch(function(err){ 144 | done(err); 145 | return; 146 | }); 147 | }); 148 | }) 149 | 150 | describe('places', function() { 151 | it('should get the right response format', function(done) { 152 | 153 | var text = 'Lets all go to Virginia beach before it gets too cold to wander outside.'; 154 | indico.places([text, text]) 155 | .then(function(res) { 156 | res[0].sort(compare_by_confidence) 157 | res[0][0]['text'].should.contain('Virginia') 158 | indico.places([text, text], {"version": 1}) 159 | .then(function(resv1) { 160 | resv1.sort(compare_by_confidence) 161 | expect(Math.abs(res[0][0]['confidence'] - resv1[0][0]['confidence']) > .00001) 162 | done(); 163 | }) 164 | .catch(function(err){ 165 | done(err); 166 | return; 167 | }); 168 | }) 169 | .catch(function(err){ 170 | done(err); 171 | return; 172 | }); 173 | }); 174 | }) 175 | 176 | describe('organizations', function() { 177 | it('should get the right response format', function(done) { 178 | 179 | var text = "A year ago, the New York Times published confidential comments about ISIS' ideology by Major General Michael K. Nagata, then U.S. Special Operations commander in the Middle East."; 180 | indico.organizations(text) 181 | .then(function(res) { 182 | res.sort(compare_by_confidence) 183 | res[0]['text'].should.contain('U.S. Special Operations') 184 | indico.organizations(text, {"version": 1}) 185 | .then(function(resv1) { 186 | resv1.sort(compare_by_confidence) 187 | expect(Math.abs(res[0]['confidence'] - resv1[0]['confidence']) > .00001) 188 | done(); 189 | }) 190 | .catch(function(err){ 191 | done(err); 192 | return; 193 | }); 194 | }) 195 | .catch(function(err){ 196 | done(err); 197 | return; 198 | }); 199 | }); 200 | }) 201 | 202 | describe('organizations', function() { 203 | it('should get the right response format', function(done) { 204 | 205 | var text = "A year ago, the New York Times published confidential comments about ISIS' ideology by Major General Michael K. Nagata, then U.S. Special Operations commander in the Middle East."; 206 | indico.organizations([text, text]) 207 | .then(function(res) { 208 | res[0].sort(compare_by_confidence) 209 | res[0][0]['text'].should.contain('U.S. Special Operations') 210 | indico.organizations([text, text], {"version": 1}) 211 | .then(function(resv1) { 212 | resv1.sort(compare_by_confidence) 213 | expect(Math.abs(res[0][0]['confidence'] - resv1[0][0]['confidence']) > .00001) 214 | done(); 215 | }) 216 | .catch(function(err){ 217 | done(err); 218 | return; 219 | }); 220 | }) 221 | .catch(function(err){ 222 | done(err); 223 | return; 224 | }); 225 | }); 226 | }) 227 | 228 | describe('relevance', function() { 229 | it('should get the right response format', function(done) { 230 | 231 | indico.relevance('president', 'president') 232 | .then(function(res) { 233 | res.should.be.above(0.5); 234 | done(); 235 | }) 236 | .catch(function(err){ 237 | done(err); 238 | return; 239 | }); 240 | }); 241 | }) 242 | 243 | describe('relevance', function() { 244 | it('should get the right response format', function(done) { 245 | 246 | indico.relevance(['president', 'Barack Obama'], ['president', 'prime minister']) 247 | .then(function(res) { 248 | res[0][0].should.be.above(0.5); 249 | res.should.have.length(2); 250 | res[0].should.have.length(2); 251 | done(); 252 | }) 253 | .catch(function(err){ 254 | done(err); 255 | return; 256 | }); 257 | }); 258 | }) 259 | 260 | describe('text features', function() { 261 | it('should get the right response format', function(done) { 262 | 263 | indico.textFeatures('Queen of England') 264 | .then(function(res) { 265 | res.should.have.length(300); 266 | done(); 267 | }) 268 | .catch(function(err){ 269 | done(err); 270 | return; 271 | }); 272 | }); 273 | }) 274 | 275 | describe('text features batch', function() { 276 | it('should get the right response format', function(done) { 277 | 278 | indico.textFeatures(['Queen of England', 'Prime Minister of Canada']) 279 | .then(function(res) { 280 | res.should.have.length(2); 281 | res[0].should.have.length(300); 282 | res[1].should.have.length(300); 283 | done(); 284 | }) 285 | .catch(function(err){ 286 | done(err); 287 | return; 288 | }); 289 | }); 290 | }) 291 | 292 | 293 | describe('sentiment', function() { 294 | it('should get the right response format', function(done) { 295 | indico.sentiment('Really enjoyed the movie.') 296 | .then(function(res) { 297 | res.should.be.above(0.5); 298 | done(); 299 | }) 300 | .catch(function(err){ 301 | done(err); 302 | return; 303 | }); 304 | }); 305 | }); 306 | 307 | describe('twitter_engagement', function() { 308 | it('should get the right response format', function(done) { 309 | indico.twitterEngagement('#Breaking rt if you <3 pic.twitter.com @Startup') 310 | .then(function(res) { 311 | res.should.be.above(0.5); 312 | done(); 313 | }) 314 | .catch(function(err){ 315 | done(err); 316 | return; 317 | }); 318 | }); 319 | }); 320 | 321 | describe('sentimentHQ', function() { 322 | it('should get the right response format', function(done) { 323 | indico.sentimentHQ('Really enjoyed the movie.') 324 | .then(function(res) { 325 | 326 | res.should.be.above(0.5); 327 | done(); 328 | }) 329 | .catch(function(err){ 330 | 331 | done(err); 332 | return; 333 | }); 334 | }); 335 | }); 336 | 337 | describe('language', function() { 338 | it('should get the right response format', function(done) { 339 | indico.language('Quis custodiet ipsos custodes') 340 | .then(function(res) { 341 | // number of languages 342 | Object.keys(res).should.have.length(33) 343 | done(); 344 | }) 345 | .catch(function(err){ 346 | done(err); 347 | return; 348 | }); 349 | }); 350 | }); 351 | 352 | describe('textTags', function() { 353 | it('should get the right response format', function(done) { 354 | indico.textTags('Really enjoyed the movie.') 355 | .then(function(res){ 356 | // number of categories 357 | Object.keys(res).should.have.length(111) 358 | done(); 359 | }) 360 | .catch(function(err){ 361 | done(err); 362 | return; 363 | }); 364 | }); 365 | }); 366 | 367 | 368 | describe('analyzeText', function() { 369 | it('should get results from multiple text apis', function(done) { 370 | 371 | var example = 'Really enjoyed the movie.'; 372 | 373 | indico.analyzeText(example, {'apis': ['sentiment', 'textTags']}) 374 | .then(function(res){ 375 | Object.keys(res).should.have.length(2); 376 | done(); 377 | }) 378 | .catch(function(err){ 379 | done(err); 380 | return; 381 | }); 382 | }); 383 | }); 384 | 385 | describe('Keyword Arguments Function', function() { 386 | it('Keyword arguments should function when passed into config', function(done) { 387 | indico.textTags('Really enjoyed the movie.', {'top_n': 5}) 388 | .then(function(res){ 389 | 390 | // number of categories 391 | Object.keys(res).should.have.length(5); 392 | done(); 393 | }) 394 | .catch(function(err){ 395 | 396 | done(err); 397 | return; 398 | }); 399 | }); 400 | }); 401 | 402 | describe('keywords', function() { 403 | it('should get the right response format', function(done) { 404 | indico.keywords("A working api is key to our young company's success", {top_n: 3}) 405 | .then(function(res){ 406 | 407 | // number of keywords 408 | Object.keys(res).should.have.length(3); 409 | done(); 410 | }) 411 | .catch(function(err){ 412 | 413 | done(err); 414 | return; 415 | }); 416 | }); 417 | }); 418 | 419 | describe('keywords', function() { 420 | it('should get the right response format with specified language', function(done) { 421 | text = "La semaine suivante, il remporte sa premiere victoire, dans la descente de Val Gardena en Italie, près de cinq ans après la dernière victoire en Coupe du monde d'un Français dans cette discipline, avec le succès de Nicolas Burtin à Kvitfjell." 422 | indico.keywords(text, {top_n: 3, language: 'French'}) 423 | .then(function(res){ 424 | 425 | // number of keywords 426 | Object.keys(res).should.have.length(3) 427 | done(); 428 | }) 429 | .catch(function(err){ 430 | 431 | done(err); 432 | return; 433 | }); 434 | }); 435 | }); 436 | 437 | describe('keywords', function() { 438 | it('should get the right response format with auto detect language', function(done) { 439 | text = "La semaine suivante, il remporte sa premiere victoire, dans la descente de Val Gardena en Italie, près de cinq ans après la dernière victoire en Coupe du monde d'un Français dans cette discipline, avec le succès de Nicolas Burtin à Kvitfjell." 440 | indico.keywords(text, {top_n: 3, language: 'detect'}) 441 | .then(function(res){ 442 | 443 | // number of keywords 444 | Object.keys(res).should.have.length(3); 445 | done(); 446 | }) 447 | .catch(function(err){ 448 | 449 | done(err); 450 | return; 451 | }); 452 | }); 453 | }); 454 | }); 455 | 456 | describe('BatchText', function() { 457 | if (settings.resolveApiKey() === false) { 458 | // skip test -- indico auth keys are not available 459 | console.warn('Api keys are now required. Skipping some tests.\nhttp://docs.indico.io/v2.0/docs/api-keys') 460 | return; 461 | } 462 | 463 | describe('batchPersonality', function() { 464 | it('should get the right response format', function(done) { 465 | 466 | var examples = [ 467 | "I love my friends!", 468 | "I like to be alone." 469 | ]; 470 | 471 | indico.personality(examples) 472 | .then(function(res) { 473 | 474 | res.should.have.length(examples.length); 475 | Object.keys(res[0]).should.have.length(4); 476 | done(); 477 | }) 478 | .catch(function(err){ 479 | 480 | done(err); 481 | return; 482 | }); 483 | }); 484 | }); 485 | 486 | describe('batchPersonas', function() { 487 | 488 | it('should get the right response format', function(done) { 489 | 490 | var examples = [ 491 | "I love my friends!", 492 | "I like to be alone." 493 | ]; 494 | 495 | indico.personas(examples) 496 | .then(function(res) { 497 | 498 | res.should.have.length(examples.length); 499 | Object.keys(res[0]).should.have.length(16); 500 | done(); 501 | }) 502 | .catch(function(err){ 503 | 504 | done(err); 505 | return; 506 | }); 507 | }); 508 | }); 509 | 510 | describe('batchSentiment', function() { 511 | it('should get the right response format', function(done) { 512 | 513 | var examples = [ 514 | 'Really enjoyed the movie.', 515 | 'Worst day ever.' 516 | ]; 517 | 518 | indico.sentiment(examples) 519 | .then(function(res){ 520 | 521 | res.should.have.length(examples.length); 522 | res[0].should.be.above(0.5); 523 | done(); 524 | }) 525 | .catch(function(err){ 526 | 527 | done(err); 528 | return; 529 | }); 530 | }); 531 | }); 532 | 533 | describe('batchSentimentHQ', function() { 534 | it('should get the right response format', function(done) { 535 | 536 | var examples = [ 537 | 'Really enjoyed the movie.', 538 | 'Worst day ever.' 539 | ]; 540 | 541 | indico.sentimentHQ(examples) 542 | .then(function(res){ 543 | 544 | res.should.have.length(examples.length); 545 | res[0].should.be.above(0.5); 546 | done(); 547 | }) 548 | .catch(function(err){ 549 | 550 | done(err); 551 | return; 552 | }); 553 | }); 554 | }); 555 | 556 | describe('batchTwitterEngagement', function() { 557 | it('should get the right response format', function(done) { 558 | 559 | var examples = [ 560 | 'Pic.twitter.com rt if #breaking', 561 | 'Worst tweet ever' 562 | ]; 563 | 564 | indico.twitterEngagement(examples) 565 | .then(function(res){ 566 | 567 | res.should.have.length(examples.length); 568 | res[0].should.be.above(0.5); 569 | done(); 570 | }) 571 | .catch(function(err){ 572 | 573 | done(err); 574 | return; 575 | }); 576 | }); 577 | }); 578 | 579 | describe('batchLanguage', function() { 580 | it('should get the right response format', function(done) { 581 | 582 | var examples = [ 583 | 'Quis custodiet ipsos custodes', 584 | 'Clearly an english sentence' 585 | ]; 586 | indico.language(examples) 587 | .then(function(res) { 588 | 589 | // number of languages 590 | res.should.have.length(examples.length); 591 | Object.keys(res[0]).should.have.length(33); 592 | done(); 593 | }) 594 | .catch(function(err){ 595 | 596 | done(err); 597 | return; 598 | }); 599 | }); 600 | }); 601 | 602 | describe('batchTextTags', function() { 603 | it('should get the right response format', function(done) { 604 | 605 | var examples = [ 606 | 'Really enjoyed the movie.', 607 | 'Not looking forward to rain tomorrow' 608 | ]; 609 | 610 | indico.textTags(examples) 611 | .then(function(res){ 612 | 613 | res.should.have.length(examples.length); 614 | Object.keys(res[0]).should.have.length(111); 615 | done(); 616 | }) 617 | .catch(function(err){ 618 | 619 | done(err); 620 | return; 621 | }); 622 | }); 623 | }); 624 | 625 | describe('batchKeywords', function() { 626 | it('should get the right response format', function(done) { 627 | var examples = [ 628 | 'Really enjoyed the movie.', 629 | 'Not looking forward to rain tomorrow' 630 | ]; 631 | indico.keywords(examples, {top_n:3}) 632 | .then(function(res){ 633 | 634 | res.should.have.length(examples.length); 635 | Object.keys(res[0]).should.have.length(3); 636 | done(); 637 | }) 638 | .catch(function(err){ 639 | done(err); 640 | return; 641 | }); 642 | }); 643 | }); 644 | 645 | describe('batchAnalyzeText', function() { 646 | it('should get results from multiple text apis', function(done) { 647 | var examples = [ 648 | 'Really enjoyed the movie.', 649 | 'Not looking forward to rain tomorrow' 650 | ]; 651 | indico.analyzeText(examples, {'apis': ['sentiment', 'textTags']}) 652 | .then(function(res){ 653 | Object.keys(res).should.have.length(2); 654 | res['sentiment'].should.have.length(2); 655 | done(); 656 | }) 657 | .catch(function(err){ 658 | done(err); 659 | return; 660 | }); 661 | }); 662 | }); 663 | 664 | describe('intersections', function() { 665 | var examples = [ 666 | 'Really enjoyed the movie.', 667 | 'Not looking forward to rain tomorrow', 668 | 'Our apis go together like pb and j' 669 | ]; 670 | 671 | it('should get the right response format in api mode', function(done) { 672 | 673 | indico.intersections(examples, {'apis': ['textTags', 'sentiment']}) 674 | .then(function (res){ 675 | expect(Object.keys(res)).to.have.length(111); 676 | expect(res['golf']).to.have.property('sentiment'); 677 | done() 678 | }) 679 | .catch(function(err){ 680 | done(err); 681 | return; 682 | }); 683 | }); 684 | 685 | it('should get the right response format in historic mode', function(done) { 686 | this.timeout(5000); 687 | indico.analyzeText(examples, {'apis': ['textTags', 'sentiment']}) 688 | .then(function (res) { 689 | indico.intersections(res, {'apis': ['textTags', 'sentiment']}) 690 | .then(function (res){ 691 | expect(res['golf']).to.have.property('sentiment'); 692 | expect(Object.keys(res)).to.have.length(111); 693 | done(); 694 | }) 695 | .catch(function(err){ 696 | done(err); 697 | return; 698 | }); 699 | }) 700 | .catch(function(err){ 701 | done(err); 702 | return; 703 | }); 704 | }); 705 | }); 706 | 707 | describe('batchPersonality', function() { 708 | 709 | it('should get the right response format', function(done) { 710 | 711 | var examples = [ 712 | "I love my friends!", 713 | "I like to be alone." 714 | ]; 715 | 716 | indico.personality(examples) 717 | .then(function(res) { 718 | 719 | res.should.have.length(examples.length); 720 | Object.keys(res[0]).should.have.length(4); 721 | done(); 722 | }) 723 | .catch(function(err){ 724 | 725 | done(err); 726 | return; 727 | }); 728 | }); 729 | }); 730 | 731 | describe('batchPersonas', function() { 732 | 733 | it('should get the right response format', function(done) { 734 | 735 | var examples = [ 736 | "I love my friends!", 737 | "I like to be alone." 738 | ]; 739 | 740 | indico.personas(examples) 741 | .then(function(res) { 742 | 743 | res.should.have.length(examples.length); 744 | Object.keys(res[0]).should.have.length(16); 745 | done(); 746 | }) 747 | .catch(function(err){ 748 | 749 | done(err); 750 | return; 751 | }); 752 | }); 753 | }); 754 | }); 755 | -------------------------------------------------------------------------------- /test/integration/versioning.js: -------------------------------------------------------------------------------- 1 | var indico = require('../..') 2 | , settings = require('../../lib/settings.js') 3 | , fs = require('fs') 4 | , path = require('path') 5 | , should = require('chai').should() 6 | , expect = require('chai').expect 7 | ; 8 | 9 | var data = fs.readFileSync(path.join(__dirname, '..', 'base64.txt'), { encoding: 'utf8' }); 10 | 11 | describe('Versioning', function() { 12 | if (settings.resolveApiKey() === false) { 13 | // skip test -- indico auth keys are not available 14 | console.warn('Api keys are now required. Skipping some tests.\nhttp://docs.indico.io/v2.0/docs/api-keys') 15 | return; 16 | } 17 | describe('version present', function() { 18 | it('should get a valid response', function(done) { 19 | 20 | indico.political("Guns don't kill people, people kill people.", {'version': 1}) 21 | .then(function(res) { 22 | Object.keys(res).should.have.length(4); 23 | done(); 24 | }) 25 | .catch(function(err){ 26 | done(err); 27 | return; 28 | }); 29 | }); 30 | }); 31 | describe('version 2 for image features', function() { 32 | it('should get the right response format', function(done) { 33 | indico.imageFeatures(data, {"version": 2}) 34 | .then(function(res){ 35 | 36 | Object.keys(res).should.have.length(4096); 37 | done(); 38 | }) 39 | .catch(function(err) { 40 | 41 | done(err); 42 | return; 43 | }) 44 | 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/jpgbase64.txt: -------------------------------------------------------------------------------- 1 | /9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAPAAA/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoKDBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBWQFZAwERAAIRAQMRAf/EAKMAAQABBQEBAAAAAAAAAAAAAAAFAgMEBgcIAQEBAAMBAQEAAAAAAAAAAAAAAAECAwQFBhAAAQMCBAMFBQUGAgkCBwAAAQACAxEEITESBUFRBmGBIhMHcZGhMhSxwUJSI/DR4WIVCPFygpKiM0NTJDQWcyWywtKzRCYnEQEBAAIBBAIDAAIDAAMBAAAAARECAyExEgRBUSIyE2FxgbEFkUJSFf/aAAwDAQACEQMRAD8A9UoCAgICAgIFAgUHJAoOSBQckCg5IFByQKDkgUHJAoOSBQckDSOSBpbyCD5pbyCBpbyCBpbyCBobyHuQNDPyj3IGhn5R7kH0ADIU9iAgICAgICAgw943a02nbZ7+6NIoW10imp7jg1ja5uccAqcnJNNbtey/HpdriPPXWHW+97pu7rySXRFCdMMDKOjjaD4m4jxH8zl5PJNuXrt/8PV4tddJiO1enW7zbl01CZgRJbkRVJqSwsa9h/1Xgdy9D1N7tp1+Ojz/AGdZN+ny2ddLnEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHIvU/qUXm4us7eUGz2xzmOIodV3p/UcK5+TGdI/nd2Lz/Y389/H41/7eh6/H465+dv+nH73cGEza2ltK6Wn5hlzryUYdEjsfoHvzLrbruxlcRMS2SJrjWvlNEUlB/KNHvWnqXF21/5cnt69r/w60u1xCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgII7qLdm7PsO4bo4avoreSYN/M5jSWt7zgq77eMt+ltNfLaT7eaJr2dm0SSvc+WVzWmeQgPPnSnzZpMTTEn8QXjce3TP29nadcfTmd51K+43KZkUnnHVoiNagNHAYCq7JrWPl8R0X0y3+62vdX3MLayQQ/WsaOIjwmbSv443Ee2iw5NvDabT4WuvlLr9vVkEzJ4I5mfJK0PbXk4VC9Z5CtAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQR+/71bbJs91ulw0vitmatDc3OcQ1rR/mc4BV33mutt+FtNfKyRyO99bt+jvnNiigEbZCwxmF7m0H8/mNPf8F509rl26yR6F9XjnfLbumfVva9y0x7hGLN1PHcNdqiB/mBAewH+YU7Vtp7nXG88f8Apjv6nzrc/wDbfIpYpY2yRPbJG8VY9pDmkcwQu2XLjswqQEEZu3U3T20RufuW4QW2gAuY94145UYKuPcFTbk1171bXTbbtHJ/U31Ig6m6el2jpdstxBLIG7jcGJzKxtILWx66HxOGJpl7V5vu+3PHx1+Xpel62NvLb4cxL6bfNt9y2WHzWOY3zAQCSKE48gFw+vyTMd3Np8xy28tpNm3vBxh8wF7CaEhhJAwIpjRevLmPP7VvHQ24SW8t5uEpc8Ms5WRsY1zy8vo0BoGPDFcvsTOuI1075eqfTjqJ+89PRtmAF3t4Za3BBBDy1jS2TDAa2kGi7/X5fPTLz/Y4/DZtS3YCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICCI6t2o7t05f7e0gSTxHyXHISs8cZP+m0Km+nlrZ9rabeO0rzDvl7BasbuEADIb0Nlga/HTVuLT/kdX3LzeLt/p6u9RW3dX2cksTHxSC4ZQOuYHDzcvxBxo6prir1XDeunPUbdtguBNb/AKkRr59kT+jK3g9tKhj/AOZveFzTl24bnX9fppeLXlmNu/220+vN/uNWbLswY+OnnOuXl+mvJrA37Vrt/wCn9Rl//Nx3qC6h3X1G3yJwk3F9s1uPkQ/oNbUHPRRzh7SuHk97fa9a6eP1uPX4QPSe1Ws8jL+Uta8AxyxPxcJY3aJGOJ5OC5+TexviN9s7azjj0NjAFQSG5YU/csrWdqncNisb6J8FzHUyigfTGvNRr0uTzcq6t9Jp/r576Rrrm3LGiO4GAYG0Glw9gwovV4fb6YrHfjluYx+kLGzsfoY7psjL60Y9gLCdMr5HAguofwA05K/LyzrhGvHcO8+j8VHbxKyPRFIbcGmRkaH6ssK0IrRdP/n9r9OT3e8+3R16LhEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEFq7e2OB8jjRrAXOPYBUpEV476igm3DoizELtEg8x8claDT5jnBvfVeXxX8r/t6+2vT/hyHzJIbsCQOc8O8QJIy7Qu3GY5e1dRdPJdM26G0c7z5Whge4AFrXCvjIwcRwdQLzfY6Su7h7updH7NDs+3tbo1TP8A1JpH4F1RWp+YYZUqvH2za6ttkvc7g0ai0N1H4ZimPLtVdpiI16tL3G6Zbbg/y/AJnGR9PkMlRqPLHD3K2c6/6bzXFbZtkkriPJfUOoMePM/BV11rDexOfUOZGxk5q3nSgrz7exWv+WWPpKW8sE8BifR8ZBDmnJwW2ljLaVq3UHQMRrfbV4QcXwAc/wAoGSjbM/zG3Hyy9K270hubhu23m2zQCIW0gex1KPd5ldWvHEinuXsf+Zy+Wt1+nB7+mNpft0Beo88QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQW7gAxEOFWnAjmCpiK83bxabfYbluvSl3ZzzWtjcONnPZMq5kcrTMxspP4QzVieS8fkl05Lh62l8tJXA+qtrdb3R3CBrpLGS5dGA8EOqKOxwbUeLArs49s9GHJMdXR/Ta3lh8qeQefDA3VAXtApqybWnhovP9vby6OvimI6La7vNMZfMOgSUYezhmV53jhun7LZjcNBBDyMG1ILTWmXGverfyz1UvLha3TomKS0fpirK1+tvYDgVnrx1pPYTfT3TVvBG0Pa0tABx54VXTw8Uz1cvNzWsnd59ufLNZOibE6EaiXEAEGoBxyFVpzcUwpxb3KBttwaydkDCDQ/hcCR7BQGnsC4MYdtmYmbHezG57JaatVM/mHettdrGG3GzenNyZa9SsYQBDuLTHQNI0yN8TBgu70N/Hkx/+mHs650/039e48wQEBAQEBAQEBAQEBAQfKgIGoIGoIPhkaEHwTxnIpkBMwioKCrW1A1tQNQQfUBAQEBAQEBAQfHtDhQoOSeqPT1xt+8f+UWTXG2lt3Wu5iMEuY4A+XcODc6NJYeS4Pd4bcbR3epyTF1rh3VkQ3q7t4mO8y2h0kSBtD3lcOvN45dv8stx222jsNqgto9A50cC4kiuI4LCXy61NmKx4bsCYkEueHUFM8+GYVbF3Ttj3+zay2sHDy3yiprpNKc8ahdWk6OHknVsc0rXHyw+jqYge2lfZVY7TFwnXtli3MxjayS2NZIzqc05lpwOdMuKnyxipmue7k/rZ1Y/admkjsbj6S/3J8UU24RND5mQEuJewCmLWk0xBxXd6+s2uWXJbI5D6Y7tvZ602uCC7u7pl5IyC8huZDO3W80dIw6jp0jFuNTiCt/Y4dd9L/hTi5NtNp/l6ak6Xvw36yOcFr9QMbRpo8YU7/avFnDe7v/tOyy500ENrO2Kk1pKyRhbWhcxwIDqBTptZZfqlkuZ9uuxSCSJkgye0OHeKr6SXLxrFSlAgICAgICAgICAgICCl2aD4UFp0jQc8sSmRH3+5xxN0B1HHPsWW/JhfXXKOdvApoBoTmeTVjeZf+bMt9xaKajicWt7FpryK3RmtvGmhJz/bBazZTC4LhpOatlC+11UF0ZICAgICAgICAgIMS4Y1+tj2hzHYOa4VBB5hSjLzd1J007bOqLiyla0RCZ0kYppYY3HU2gFaZr532NLrvY93g38tJX3cnhgAJoxoNA2mGGCrrEtZ3Hc37ZtFzuIYXlkbnUGGAFTnxOVSrcevltJ9o32xrb9NA2r1P6htt1iuryOKS0keT9JGwh8Rr4S15GfedQqvWvq646PN/vtnq7z0/wCo9juojmtn0L4aua4gGlWmgFcThkvM5uOzZ2ceLE1Jv8rmiSN41Vpxyca4fYsl/FqHWvT0W+38Uk7XNtbgNaLmOuqCX83MAU8JotOHmuvb4NuOWJDovoDZ+lZmbw+aTcN1Ebmw3Eoo1lfmLWtwqW9i25va22nXsw4+DWXo6/she/ZS2RtH0144Y5qnrX8bFOefkj9x2hk0JLIyNXipgKH2hV5eJbj5Gx9J3L5Npjt5nVuLX9J4OekfKfZTBep6fJ5aT7jl9jXG3+00upgICAgICAgICAgICAg+OQY0s7WNcTwUWpwg7zdmsEjicGkCnsXPvyYaa6NVvN1e+Vzi6pzdyqf2ouLfkzXTrow3bq1jiXOwHPDAYLLzaeL6zqJzXB2rMVocvcrzlReNkt6rAd/vKnieC0/vVf4pKw6kbK8DUSTlRa8fPlnvxYbVY3fmNBqKe1dutctiUjNW1VkKkBAQEBAQEBAQYso8bqZqVXD/AFalfB1hBpeNUkLdZoDShK8f3p+b1fT/AEazetkmLGllRIKE8x93cuPDpjJj6VsrrZ5vrovPhkDmSxEn5HNoaUpR3Jaa51/KKbbZ/FzK89IvJm12e7A2lf0w5lZmDk0lxbUDiAvQ19zp26uO+t17pXp/Yv6ZdR+VKfLghDNR4knGncFycu2XVpMNzsrv9RurLGlcqHks9Neqdr0b9Z7aZrXSxrvKlYPGa0J7eVVbf16znNFywso2QmzmbSSI6oCaCmfhPYVh42fjV7t8xtNvK5lqxrRnRobxHNdPFMObfrUlFGyWMOkJNBRtTUDuW1mWWcMnaLeOO8uHNzc1teWZyW/qa4tV5bmRLLtYCAgICAgICAgICAgIKJXUCCB3K5LA+h7Vz8mzXXVpu6bhpEmOBea9xXDyburTVq15uoFXVw4cMjgue1vNULcby3VQuqT4iFVpIwDvT3OJc40PGuaJ8VQ3aUnwHwVFa0y7uKZT4pjbN5frGh2Xbh96trthTbV07pjczOxuo49lcfeSvS4N8vP5tcN4tjWEFdjmXUBAQEBAQEBAQYsztLnnlipQ85deXD7vqu4mcKePS2h1YN7F4Ps7Z3r2fXmNGVtMLZg0EOc9hqRxyFMP3qmkyb3DY/KLWPiLKNcAaZ551W20Y63q1neNt0zF8RI1A6hXCvArLXpW3lmIK/hDJmgMxa0Npx444K/yiXonNo2uO4mbNI4BjaBox8VB/FdXB69vVz8vNjo2+x32K0uY7VsxDWlpceFCdOIOOK77pMOPyrd7rarK+jJhoZ42hxcMBjiKrm5ODXZppy3VFw3QZIWkBrYhqfXDSfl40C45LOjepJm5tkaCx2toFasIx50zV5VPFL9PyRXMD7xjtQe4splTTnXvXX6s6ZY8vS4Sy6mQgICAgICAgICAgICDCvJA15FaYKu1Wkajv24hjXY5ELh5d3Tx6udb3uoYXeLAlxA444cFxW9XXrGl7jvR11Dqgnw0OFBgO5RhpIgLzeNMmhzx4hWg448SrzTKLthVb3rJGNBDtVfFlj2Yqu0XlZbriPTi6jTkDVw+NAs1oltuv4muaA9xaKUwwTCtjq/Qk8byDrB/lXo+q4PYdTsz+g1eg4l5AQEBAQEBAQEERv25W23WFzdzvDWRtJxNKmmSrvvNdc1OuubiPPc0hvb59w4EPlkLzQ0IqezFfP7XNy9rWYmGybVBAwNcB5ZA+ZvD35rTWsd0udJaRq1A/iK2z0Yo65gbM5zKAPBqeRHBVkzV7cIG92wy7hFo41JOdCP8VbXT8sF3/FH9edV7b0tFZ+dRkzmnyo2OFXtpmRwxK9Tj6Rw79XJNx9WeoLy5aLCsQaDGHu8Ujm1JANKZVVsK3Z1/+331l3i76jHSnUj/ADHX7C/bp3N0O1sbqMTsBUOYKsPZTknjhW12rc4I7TdGaW0Zcn/bFftXHzceLmOjTbMRXUd0LW1fM2oczL5aEn2ri22xMujj1zcNk9P3SP6VtJJI/LfIZHOFCCavPiNea9L08/zmXN7M/O4bEulgICAgICAgICAgIBKDHnuDG0kUw5qLU4axum8vL3udQFuFB2LHk3baatA6i3rW7BwoSAcag/tivN5N3bx6ubdRbyC51TRoxIHIAmiyndvJ0aRue85uDqHMNW+mjPbfCKuL64upYgwAknsGfNbTWSMrc1tNhDILL9UgyPGDajPOp/xXJtc10zozzcSROEZIqAAeDSaY5VoQVnZlaMu1u4nSNLqHnWh7wRRJEV330q2DTtx3C6ikZqI+njlYWClAdYDsTngcl6/raYmXmexv1w6GulzCAgpe8MaXEEgZ0QUNuY3ZVQVecztQPOZ2ojJ5zO1E5POZ2ojLC3Xf9r2q1fc3soiiYKnmfYFXfeazNW11u1xHnv1C66n3zdZnQSSN2xrq2sTxpGA+YgE4rx/Z5rvenZ6nBwzWf5QVhuPmOYXuNA7IGle3FcsdFjcrS7gt4GsODjiDSpP+ktO3djeq5Hu721EdCPyu/ek3Looj3F7rhocWxtHzg1OquWPDFaabRTbXouWh1X7BQB4c5rmg5CpGdBiV16TGzn2v4uYf3G9O3czNr32AarOBrrS5P/Le5wdG4jk8AgHmu6OWuJsu5hoDGmDTQAN8JrwcTzV1HTPQSyuLv1Ftd7vriWeawL5XyTajqlfG6KOLU4lznEuB7GgqcZHrqYtvr6yAeSGOc8nOtGkcKUxWHJq01rXOtsXNt3UbEw1zzce5eb7PTpHd6323/pmOWPYbFkri9wib8w0kCmDadmS9Phlmky4uW/lUmtWYgICAgICAgICAgpcgi9zm0MJWe1X1c+6ivxEXuacvmHYeS4Obd18Wrmu97qGS+JxAfWh4VyquTOXZJ0c+6i3GskmjI1B9itpOqdr0azDY3N9c+TCNRefETWjRzJXZq5tkrDt9nFMLe0YZHtNH3UmbjxIZk1o96y5d2nHomvPa2Itip8zIwTlga/uXM2VXM5c8OxLfCWu7aUcPeFGpW1+nnTkvUXUNnbMgE0AeH3tTpAhBGp1RkaFb8HHdtv8ADHm5PHV6rYxkbGsYNLGgNaBwAwC9h5L6gICCiZwETq8RT3oMVgoFKFaIESII7e9927aLYzXs4hbTwk5k9gWe+81nVbXS7Xo4d1p1TNvVw+QOIt24MFC2vtC8f2Obzv8Ah6fDxeM/y57e3FHGlXDKoP3FYZdMj5YXRdR1NONM2k+4Jhaul7C+x3SxbbPLRI0Y0NThxGa6eOTeYrj5M63MXv6BdwzOa8CSM/I8YGnaFneCyrf2ljNbtfkwjUKvBFOS104sd2O/Is3sX9NkF+5r5LZ3+8lY0vdCT+LQKnTq5Lq1jK1b6lmt5tnnglAfHMzSdQa9jq0oHA4Ee0Gi6taxrzVue2bdFcSeZSBrnn9KGr3O/LRnLDNXUsb76ddG9ZbtewRbLtT7K2FSbq9foY2gFXaGgPcKYU4pd8Jmr03se2X+2GKOWQ3J8sNkuHUBJ40aMhXgsLbatiYZF1tUV/vdlE8Dy2HzpONdGIB9qz245d4013s1rcF2OcQEBAQEBAQEBAQEHxzaoIDqMPt4debHYd6x5ezTj7uPdW7s0lzQ7M4UK8nl2y9Li1c63O7Y75iXUrT7wsnRI1i8gN1L5MbedTwHGpJyW/HKpyWSNh2/Zbfbdpe9ml81x80ooQeYDjiWjszK69piOOXNQU8b4QdLcXnDniuSzq6pQv0CJlfC2hca1qTifgownKQsbWa6nihjaXOkNGsGFSQDgk1yi7YeofSTpew2bpiG5hDjc3zQ6Zz9NQGudpaNIwpXEHivV4dJrq8zn3u2zeFsxEBAQWbkEhoz7EFoCiIfQFIs3t7aWNrJdXkrYLeIVkleaNA7SotwSZcr6h9fdjY2a12KJ9zeRksbPI0CH/MCDUjuXJye1J27uvj9W3v2co3fqnd94vvqdwuXTSF1dDidLexrQcF5/JyXbu7tOOa9khEPMtmk1IIGORx9q5miB3Oyex1W41/FQAd6LyrcEWkeIY0z0g/eFaIqb6dvprHco31cWuIDm155ZK+m2Kz5JmOv2s0VxCDxpiF3y5edZhjzNc9zmUw4FVl6rWdFLA5rTXEcRkMFoqwb3ZBevGhtKnEAeE4/lyKtKjLI2r032aK81mytWEtDqthYxzTx8TQDwV/Kq9HQrWG1srZlvAxsenABrQO40UWqjZGB/iOIzA4K+qFO33UB3V8znaWMaWAmvHuVNd/zaXX8WxNcHNDhkcRwXUwfUBAQEBAQEBAQEBAQRHVds+fY7ny2l0sbfMZTMFuJ+Cz5ZnWr8d/J5s6mmkfK97mmmOXNeLs9fTs0q5l1SO4DLniojVgW+4iyufPkhinbWumVusA/mpUBdHHcMOSZTFv1Xb3d0XzOkdrrUuaCGjgGtaKADvW93jnnHWLuV5azO027HBgw1nMk8SFjvtPhtrrflhNhe7En3V9izy0wyo5bmMBzZntLB4S06CBlmEyYi9D1X1BYO1WO63sDqEAx3ErcDiRg7mrzk2+1Lpr9EvX3Wb5Gv/r24PeA5oJupqgOzp4lb+m32r/PX6ipvqJ1sxoA6g3FrW8Pqpv/AKlP9dvtH89fphTda9TyuDn7zfOex2ppNzNgefzdqnz2+0eE+n2P1B63ikD4+oNxY9opUXU2X+srTk2+0fz1+nyf1B63nujcz79fvmd8zvPeOFMACAMuCf02+ycev0vweo/X0cvmR9Q7gCRpNZ3uw5YkqP67fZ/LX6XW+o/XgqT1BuHiJJHnvFaj2p/Xb7T/AC1+mFuPV3UO6Mbb3+6XV1DX5ZpnyNr2tJoqbb2/K2uknaMzao2tjBc0YZnLtXPbmtsdF2adxumRtwAxwIoUqdY2jbL0SQiB+YHhAH21WRYXluHmlaE0o00p8MlBKtDabgEGMav5QOKTWnlGVDsspka4tMb2nBxOHswVvGq3d0PbHSst2h1S4U8VV26Xo4tp1Z8d2HPLXAAjjkD71OVcLoa1x0kYGtQcM1eVWpvbdtypWtKrSKVN7faxh7pHYPHhc0+zCnck6otZF5BbFodq0YUBGOA4UVrhWZRkYDi52kNDTg4Y1Papl6JY23yMdO4OzLq1GRx5LDTb8m+06NwiOqNp5gFehHIqQEBAQEBAQEBAQEBB8cKtIpWoyKDzL1xCG3100s8t4kdqjpSmPBeJyzG1evw3o5xd0ElBlwIzKpG1YZtmOOomg7eKtlXCvTHG3wNAB9yZThS1rsNIyxJyClCouLqlxAxxFOKgUSB1caAHKikWX1pQnLIckRhjuY48RmSOCtlClzG0NcTwKIwtPaOGfaplRhb0Ekq2UYfWxFxwUZMMhkJA7R3Ktq+FJYSfynkUyYXLKAvnDQfFXuTa9CRt1tCI4QXNJJGBw5LKVeo1rz/UqZNP4QRQd5oiYnHN0s8xpqRkcaLKrRJ7VfNuCWPo2RnzNAwKSq7TCftWeLAYn5ea0jCp+ysonNrINRyNaZ+5dOsYbVI20XlubE75XVLTTKitFaqntH11DBwrRKiVTa/V/UsYallKuOWAKpm5W6YbfYTuD2lpI1DjiaEAla+bG6pOP6ggv+WOtH1xw5+xTLUYil9vM9zS2RzWtFHVoa1UyUzFD4mNjLWl1aY0wIK0kVQ9j5sUuonU3Ghzqa5Lk1tldW2LG8WjtVrE7mwH4L1dO0cO3ddVkCAgICAgICAgICAgIOQ+u9vRlnKy3Y0Oa7zLmlHEjJpcOS4Pd17dHb6ledr0k3LsMAcT/FcMd9GPr81an5Rj9qhK58xpSo4jD7qqR8fA78IozPHI96DFNdRrXDhVShSGuJoOPLgmUqjG2mNeQKjJhbkhbXwmgpgmUYWtGmopjz4qcowsyMLjiaEZBTlGFrR7+atlGGTbwNLQTia5EcFW1Mi4Wmp4tGQKhLGdiacOB5KRkbYx31bA3Gh4AFRveidY3Wj/ACWgEVphhjhjRUlRY1+4YW3XmHwiuNUXT8JEkDQOIzwVKMi3taNDxgQcHA4+9VLWy7O50lGuGok1oMBhxJNVprWO8w3fbLTUGuI1YYAZfFdekcm9SX9Po4yPrkQ3hSqvYrNlqMtAIuXaRgWONAKHChPtWcuO6bPpKwbcxz9biGjTwxyOP2q90U8khaRRRFjQQ0HjifF3lRIWpHyGubVzi8NrUgmmPMLWaqZXHOkEQwAH5mHCnsWkVRlzeuaS5kbntbmXA/CijbbxmVtdc1HBge5xLNLneKow+C4516t7cN2s/wDtIP8A02/YF62vaOO911WQICAgICAgICAgICAg0L1mu9ng6Rkbfkmd7qWjG1rr4mgIyHNc/s2ePVv68vl0eVJnB8ruNDl2rynqq2tcXUrQca4GihLKiiOknUadnHvUZThSS53gGQ7kyliSQ6n4fH9ipyYfYoCDiQQMv2KjJhkPt2uoaHmoylR9M0OqQHe+oqoyYWHx1OihrkeJVpVbGJMxzjSlDzp96tFbFtsXDM8VNqGR5YaK0xULYW5QCwDJx7UFrQ0AfFMoZW2xk3TdLyOxvDvUbXomN4t26IA86S6lfFmfes4itc3OQuncWjGuNR+4K0W+EnYS1iZqJGrhnXuVaJ/baOA8PgJwOR95UYVqf2+BrJW6RXhiQB8KK+vdls6NsFuGwNL6vJFdRwr3Bd/HOjh3vVPNsmStIcKNA8QqtLGeWHe7NpkoBVlBXvWe2i+u7HhhvrTVRpfGHatB4dg7FEzE2ysht/A2Il8T2ltX6aVNTiaEdqtmK4fYt7kZrbBBLcOqC1oaRmOambfR4suxg3u6Oq8LbaBw/wC2Z4jXmXkA+5X0m1VtkX7qFkURZQ0wGJrX3q2/ZGvdHXMDIwJR4SBRwzqsteNp5Nqs/wDtIP8A02/YF3SOddUggICAgICAgICAgcUHxzg1pccgKlB5a9YetZd+3uT6cObawfowMccPDm6naV5PPyeW3+Hqevx+M/y55BC81NaE5D+CwtdEjPtbWQZkE8iBU+9VtWkZsdjkXYU4gY9+SplKi5259Q6lWHGuJUZWiPmhMIIaaj8XsKtKYfIKFtDwxbiEoyGvqaUJpxUC6WRmoAFOPagxfILi4nBgxpxKnKGFLCHOdpyH2K2UWPkVucSRUZ55JlGHxzCTQDDtCZMLU8TiK8uyn2KZTC21tBTj2IMnb6tnaTjjlxoo2pG5QkOg8TmmgoGmpIP2LNFRO4xtdRo8RrlTBTFmXYw+FopQZAUqUqEzYkNeDgTzpn2DM0UxWty2ZjZZADRp/DWta8hgtdI5966Fskbhbh7qCtPAOa7tJ0cW96tgsg0MAfnWppzV4pWa7y3t9pqpvVVVpYT8uDlYWxZWnGMY/anjDyq9HFFGKNAaFaayIy+veAPsU2kjBn0Tgh2A4KvdKOdaT3MjbaM4Gupxx0gZ+1W1nUtbJGwRxtYMmANHcKLVVUgICAgICAgICAgIHFBqnqT1O3Yem5nsr9VdgwW9BgCR4ifYFh7HJ46tuDTy2eVtxjE9wXAVcDQf4rx8vWiq1s8wfmJxIxpVLRJQ2bY3gtFC0+MZj4mizq0ZjIGl2onMGgrywyoVFTGT9NE+IEtFdPZx9qolDbptIo8gccQVMq2WvvhEcxBGlv4Rl9y1lRV2OUCgrjkRjigzWYsq0guOFAKUUA6Lw0OPuSFWvpA0AnDD2plC2LbAluPI8VOUMXyhV1eGRRKxIC5uinbgpQtiMChpQqcmFdtq8zPE5qKRs+3ve+MF33KhVN7FGABlq95UxGWVYsDmAAUBoDxJ9yUTEMbiQWHSRQcz3kDgijdNktA4BzsHClAWudniTjVbaOfkdE25sZjYx3Maj29q9DXs4du6Xjo0+E+BShd0UxacCKnsCYQuQzjEH5hi5WlRYqMgB8RzyTItNmOrSKkHFv7kicLE0soe2jqsP7YqwtTF2ummoI44ZKYhf2ZuqaSWnyt0mudSa/ctYqllIICAgICAgICAgICBxQcU9bt7N5uMWztaBFZDzHvzJe8ZcqALzvc3zfF3erpiZcnEABwGJyFFwV3RJWtkxwNSGkVo40BJpxqoQ+Xd1FaQt8J1DMgClTwxChaMOO6AmaTQhxGf7seSpV5EhDeueQ4NGRdiMDywKqnC1N5s0WqnAkVoMlVaIHcttnONCORWmuxUNPZ3EMmrItILmnHtw9q1yzStkXvY3wgNPHCqpVmcI6gNDu0kDlnxUZRVckep2kY0GJOf3KULD7VwZ4cne3BMjDktnjE5HPD+ClKz5BFTzyQY89sWkE58EynD7bQuYS7EV4paYS23VD2jj+3JQipKWEveQ4AO/FUZN+5IqyLSIs8RIcDkccR2DkiE1ZOYQ1rml7xiQMA2uIBp8VKtb90/b62Alg1Ox5Cn2rbjjm5K3jaYWaAC0AZA1Xo6dnFt3SnlOEnZxVrFVQYCKZVxUYMrbWNLncH1oTzAySJW5g3VpIBrTUDyCgjGAkDvC7w4kOrl2JBVGXfM7EfixWkRVDiQXimGatEJjb7cQWrG5ud4nHtK1irIQEBAQEBAQEBAQEBAQcG9V4g3qy7p+IMJy4tHBo+1eV7f716Hrfq5zPLoeRlnXmAFx5dcS9i2R0A0irz+EZfYMkoit0jIuIw7AE/KQMCe3kq7L6Ma5DWyMkYCA0g9iiLpaDQ6EFgJaWitOY7FSjMaYwytKCtHZcMfiq5MMK7dG5rxpOr8xy7ElThB7naOkex9KAgduC112RhmW22OjjoACOI9ijKMklsWEECvPEJkVwR6hSpB55KYiq5IwG0IDhzrQD4ILb7eMx4MBccMf4KULEls00qKHgRkicsd1tqdkESo+nGrPu/iaIM2wh8Qoc8mjEqEWpdsIDf5OWDcOwc0VXLWHVUggkVIGeHt4KSpK0bIw6AAKGppzriccykVrfumJw9oAAJH5jXEe7FdPHXJyxtm3TzseS4+GuFMPiV1abYc20bFCdbAScaLpnVlVL4qHDI4qtiZVuSgzzFDqUCzLAXtBdgWmoIQfTbsaw4ihxoOH71aRC2NLflAAIzCsKRGHSsZTVrIBbzFcVbVCcAAFBkMlogQEBAQEBAQEBAQEBAQcg9WdlfBuw3AnVFdtFOBa5gAp28153t6dcuz19umHGNwDvqyWmgBBxplVefXoa9kztNwGxGhJByocqUqmSxjbo500xe3SC2tAMe72lRtU6dGNFHqDXOq4HE48achVUXSdvGxjBSuHDhWtVWpJXVIoaAZVxyCotGO3XK6gqT9wQrMbt7ZWtjpUNwVp1Utwy5LOKB9Xf8AEFOFMeK3mmYx8+rGls4y8tFNQHBR/Op/owLi0khkBaKt+9VssXm0qlkhdHSlK5jL9yFUNj/LieVaBSVcMA41yyzQyodAMMccgopKxzaO1aiPbRE5Zthauc+hAaBiRXGg4qYjapK6ia2MuGIAzGAwHNLFNas2bzUDLkMgOSLVMWoIefmLQ6oBNMTz44IrWz9MXDYZ9NKgmgoK0Aqalb8VYcsb7aPa+RjgAGkgDmTxXZrHHU3E9oYKUoVt2ZL7X1zxIKmbIwpf5dBrIFOBQWw+MMoDUcPYgsTS6aEkBtMaqcpWopWPoQ4PByoRh7lIyrN4N20gVBaQf3q+qtSS0QICAgICAgICAgICAgIOdesV09tnZWtAI3ue8vIqatoKA8M1x+5ekdPrTq4ZuUUbZPNpUg5nFeTe709VW1v1Q6NXGtOVKE9inAvX2kvIDg0NAr3nnRVqYwrbVSgxNK48aqF0lHpaAHEUIqe2nDFZ1ZauIxpbSuJpzy4qqYv2ls4AFrXGlSRhiTgiLUy9kdjtz55HeFrSTjwP+K6uHiy5OXkat1b1DFbQ2jInf9RK0Nb2uLQ3HsGrFd+nHhzXZg9Pb5NeSBzGHUTlnWp4nmr3jiPJu8dqyaEam4mgI7Fhy8S+nJhDX+3Ot36ox4RmByK4rrh167ZUxxMLPZhhkownL6+E0qG4jD/BSZWvJf5goKimB/dVQlQI9RFQXcgcuVOCGWVaxuY8t4OwIGH7ZpEVIXjGm1zFRg1oyr35q+0U1qLtiIjpJwODjX4VPaq4XTNiXcXeE4HPlVQipzapWxyAuoxrvFnXwg/etNKz3dE2uWJ8Q0UpwHwx4dy7eO5cW8wlfqoYhi6gGY71taykfJd4t2Mq1w7CoyYYU25snk0BwJ4NqFKcMllwWWzQ/Ak0AKsqxr+8a2DS9wJGB5d6kjDsLx4I0/Lkefcmqa2PZGyPMkzsW/Kw8+JW2jOpVXQ+NyCD6gICAgICAgICAgIBHI0Qcy9Ytvc2O03HzXPJJgERHgY2hcXCnEri9zTpl1ett8OM7mNQbG3Eu54Ci8uvR1YdlI2Jwa4nkKZe080sSvzFzo9ZNGk0bkCe1Uq0UReH8JrzHP2qLVkhbsqBUHxZjsKqnKdstmEmBbU8xiKn9yTXLPbdJRbZExgNPFgacKHh8FrOJjeRr3WE0V036GJxYxrqSuaeDaUp7V6fHpiOW7ZrlO/bNvs25+YI3TM0lsPljBozpTnVxWsqvi6X0d0t9BbQmVvj8trS3/aolRW3wMY1zzSrBQHvdQ0VULN3ZNfC8EVcW6S5cnNxt+PdrdxbfTzaW1LSSfsXJY6pcjKuOGA/NTE04pEqCxwND7kqX3ysScdTj+wRGV6GhwB8WFT2clMRWVI1ogIbwxJGHxVrOimeqFEjDL4xQCoA/fVVaJbb5KloFDTMimJP7lFE5bgB4FBUYAkA/wAMKKJUWN02iLSxpL3N1DE/iJ7V1cVcnJGbf7hb28dPM8UmIri493tXVOrDCGuLl72eYAWtHCp+NFeQXba5OjiS35qHnzrwQShvS2GOpLmVwdmB2GnuVlcITfN6tXyM0upjVxOXL3pamRf2O8uLy4FvbRh8jzQ6cqfmJ4UVtYjZ0y3hEMEcQ/A0AkYVPErojFcQEBAQEBAQEBAQEBAQEEB13tLdy6XvovL8yaKMzW+NCHxiuHdULLm1zrWnFtjaPNV1LGDpcaEd2JXh168RPnsdPpYATXSCcO9ThZJkgwOZ8zzwpxWaYsxya2kU8XxJBVavE1YROLowDiThSnFTFNq3bbbYthBbXxAeLjU5+4UXRx6uXfZrXWXVMlpcfQ2hpK0AufwaCKgfFdfHxfNYXZqDr6R87pHEu1PoDzph8V0s2zbTtX1kWpvheK6XAYg8xWqjCfLDaoLJ0cbWHF5GfMYD7M0quVb7UMbpryLu414exVS+t0P8BwP4xxHBRZklQW5WTtb9IwpXuXDycfV1ce7DhgLm4Ag4Y9pWOGtqoQjGuHCnFDKr6JtKH5jwyPwU4R5LYtmtxpU9lcSkibsuuBMZZkRyHFTcq5a7PDLHfANpTME4092PcqxplnWFxR4a19G1qTklTG12T2yMFHHVXh4Rjyw4BVlRW0be65po8LWM8IccAMMScSVvx1z8i87a4XXf1QnM4IDXAmjG0/K0Lv4+rl2uF3yWOlLNIfEKH9vYtFUeS2KeVoGgB2ANMD3c1VKN6m3GSK21a9IALm6eLgMOSbXCdZlqdmzdLqYzBj5gQXHTg8c9KznVfs656X2ETbO5vWEuEhbGwuB1DTi4VPcunhY8jeVsyEBAQEBAQEBAQEBAQEBBjbmyZ+3XTIQTM6GQRhueotNKd6jbsmd3jzqa4mtJ3scC1zHFhacDrGBr21Xh3Xq9rW9ERZX7QDrqH1GnnzxUbRaJuCZtdRcRqxrkSKZrKrYTFrAw6Cxh1OOJ/iqJbRtO2u1AvGmnyimIWmurHfZs95c2u17VLePGlsTKhvEk0o0fBdvHq4t641fXUl1cPuJzWaVxc6vJdkjJYjc1sjBwGJJ4Dn7US6R0Td2zrMguANQADycNXFSps2h7omnUSNWeeSrYSo2+vHMm8tskbQ4VFCK0GFVXC2WXsvRHU1+23nt2Mt7OZskguZXDxVHg8A8dHHjTJa68au28RW9xX+0XslhfMH1LGF1W1LHgCoLSQMD/AAVduNM3WJIHRxwyyQyRwzt1wyOa5rXN5tcRQ5rm5ODDbXkVx2rXOGkioGPZXiue8VjXzV/086atFSePFLoea63bnGmBAHFT4I8iTbXaDpAATwPJBXdgxlwGuALnEY0Jris9o11p/Rg6RzoTQ1FG8TxrWqytaRLWUDwA0kVBAaHUp248ThmowWtotoHSMjYT+m8FrqGldQ/guvj1y5OTZYn3WK0tnWbD4mnMfkrQ0PPmCu2dnPV63vo4GuBedbfE0fmBzp2qyEddbhG1krnirGg1Bxyo7D7lCYhJ557uuA0VzwoeNDXuWO1z0ayYZgsLpr4HxPc24JpH5ZpVxcG6SO2qRDtO0bfHt+3QWrM42jW7CrnnFzjTmV3a64mHLbmszFWQICAgICAgICAgICAgICAg8p+u8EEPXt2yOHyIzpkLSANTywFzgG8HFeX7E/N6fr38XP4oSPGcQeJ4rmrp1SuzxSXM9HVIbj+4LKxpl0LbLA6G6sNNRwpngcSomrLbZu21WDHtbUkEY/f3rq49HLybtU9SN0ZWLaon1NRNMB+EYhgw54ldnHrhz2ucXT6PoOOfYtUPjD5hwObsTTtxKCX2kzCpY5wFWuBB7h8KoNnjFzJDR8jvkFKk5UVbUN79J+j7a6bc7pucAuYWObHZMmGpoe2pkdpOBpUAd6vxa/Ku+3w6w1rWtDWgNa0Ua0YAAcAt2T5JFFKNMjGvbiKOAIoc80FFzaWtzbPtriJktu8aXxPALSOVCghLvoXpya3MUNv9I7/nQeF+RwJOqoxWd49atN6wmen8EeDbxxb/ADRgn3ghUvBFv6VW/oVmk6LkF1a0cyg+BKrfXWnKxP8Awm9eaOMbRzJr9gUfwqf6tP6v6evNtnibOBpkDi2VmIPsJpj7Vy83FdW/FvlAWEzxJ5ZONdJDjUn4Li3jrictmwukDXAan4VoezsVdbhGzP3i9+g2+GWLF5OgDm7TkvR4Z0cW/dqcbLlz2Oeal9XF2eNamq3UZW57vFt1mx7gHNeS3Sc2kCjh2dim9ESZa+zcrm/l8tmoQ6vC33ZlZ25aSYbLabc5sYY6MvP4R2nCtO9UzhKa2GGVnUO0xBtXec3UAcAGhzq/arcd/KI2n411xd7kEBAQEBAQEBAQEBAQEBAQEHlD+4O+8/1FuW+WY/IjhiBfgSGtrqpyJOHNed7HXd6Pr9NGjWsUk0fbkP27VybOrWty6W2xwAJGLs+ZI4BZfKdq6PtO11DTmc8HYjgttNMubfdLbtutjsO2z305pBaxkuDR4jyaOFXGgC69JJHNtm1ww77c7vfXm43H+9uJXPpwa05NHY1tGrTW5Ttrhiy+JxpnmO5aKKYAWvw4Nw9tP4qEtk2oEODWjBxB9lP8VF2MNktfMcxkUQ1yP8DGtFSS6lAKdpVZclj0DsO2R7Xs9pYMFPIjAeebzi897iV1yYjntyz1KBAQEBAQEGn+pw/9kt3AeIXDQH8RVjvtXN7M/Ftwd3LBGWXBcCQXYk0xHPEFeVtHo61Jw4Frs8gDjU415rNNT0u0O3WCBgdpZE4ucW44kUH3r1fX1zHn8u2KzbTpi1hxk8ZAoOS6ppGN2ck6gge7ery3kNGslIDK1HtXNtOronZNdJ7dEJgSC6MNGk0xJ/airgy3O1EbYnaxqpkTwOH8FTBlkdFW8l11i+5NC2zheXkVA1POgf8AzK3rzO+TluNXS13uUQEBAQEBAQEBAQEBAQEBAQeSv7hHW7vUe7ZA8yFscRnLvwvLBVg5gCi872L+b0fX/RqewuBA1uFS720GS5d46JXS+nLTU1nl1Jdxrz+4Ksiu9dB26E28NH0LgKmgxNeFF18ejj32aL6s7b1RcbWJfoLhuzwnzpJtB0OdSge+mIABwr+5a8mlwcW0y5XsLSbd7q1BcaAcaFTxzonkvVItgLtR7KU9q1jJft7Jz5qAdwzzoqWLStosLPyxqwywJywVMJtdS9K+kvMf/Xrxvgic5ljG4CjiPC6XuIoO9bcWnyy5Nvh1FbshAQEBAQEBBp/qhT+gQVP/AOUzw8/A9c/s/q24P2czcABrDgCMua8vaO7VQxzTKGipOVat8JI5LGtHRulINW3Pdn49NedB/Fez6s/B5vPfyS74TQhdLB576juH/wDk15V+Uz9bjyaVx7d3XOzb+l7hoZXDIA+2vLhjgowhserTFIewuLeNFXYjY/TKzlG1XO5y/Nfyny6f8uEljT3nUt/W1xM/anNeuG4roYiAgICAgICAgICAgICAgICDx16wTQz+oG+TW58xoufKa/VqJcxoY417HVHZkvL5rnevT4ZjSI3ZbJ7XNq3Vp0lwAw+Kx2ax1rpLbZGiKSngIzOHDDH2q/HpmsOXd1TpTbmTaryVvhidohZw1DEuI7Kr0eLX5cW9ZvXV0bXovfLgNa4x2NwdL/l/3bhitN+1V07x5A2KAR2UQd8xGsg8AcvgVhxzEdG9zUrFi/A1FQfirs0xt1nqfUZ0A9w/YquyY2fYtju943K22y3xErv15AP93CPnee5Vkz0Tbh6Cs7SC0tYbWBuiGBjY428mtFAuqRzrqAgICAgICAg1L1NjDunWOJpouYzX2hzfvWHsT8WvD+zlpZJWtBprUn5Th8KrzNo7ta+2cQfc0OJ1dmfsFVlNcr24jqnS0QbtQPBz3Ed1AvY9efg8zlv5JN0deXYtmbzX1IXHqu+DqE+e8ONCPxclyXu652bd01GWxsaRoPE/fXvSxVO7jI/+nGONtZpv04qGvieaD4lZ1bV1XaNvj27a7SxZTTbRMiqOJaKE95xXfrMTDm2ubllqUCAgICAgICAgICAgICAgIMDft3t9n2W+3S4dphs4XzOPPS2oHecFXbbEytrrm4eKi653S7ur2VxdI+fzZf5nvcXH4ryLfl60+m3bFtznTxve3kOzDDFRJlG1xHaOjdjkvaRAkQsobh4oKVrgO3uXfxceXBybulW1vDbQMhhbpjYKNC65HO1f1W3SDbvT/eZZqHzoHWzGn8Tp/wBOn+1VV37Lad3k1lwW0YPC55wA4MFaLONamtmjMlXEfiBp2Y0Cmobbt1rLVkTIzJcTOAZE0FxJdgGgZ4rOpdv6C6R/oG2ufckO3K7DXXJzDAKlsbT/AC1x5lbaa4ZbbZbQrqiAgICAgICAg1L1Njkd09GW10tuYzIBjUUcPtIWPP8Aq04u7lz2UqeRwwwAPIhebu7daztojBl1aRpqDpwFf3DtWc7rV03pip2WJxFKufl/mIXr8H6x53L+yU04rVR5o6ltv/3HcozhS5kwBNR4q4FctnWuqXo2/YWn6fUPGCNI7HD+KjeIlbb07aNu9+2+KTKAuuHNrmWCrf8AaIUcczsbXEdKXY5xAQEBAQEBAQEBAQEBAQEBBw/+5LrYW1lb9L2sg13FLjcQDiGNP6UZp+Z3ip2Bcntb/wD1dfq6dfJx/pKwMltK548T3h4BwwAouDaOyV07p3ZSCM3N7Bz4YrXi0yw5N3a+mtkZtdlQj/qJqGXkKZN7qr0tNcRwbXKXV0POXrr1wN73sbDZSatt2l5+oc0gtkuaUOX/ACwS321WW9zWukw5fZQvubrWwVqdDABhQZ/uUSJreNm2eQ+XDE1z5HkMaxoq5z3YAAe0qcod26G6Eg2OJt7eAS7q9uebYQR8rf5ublbXXCm22W3q6ogICAgICAgICDVvUqSOPpWYvzMsQYaE46q8MsAVlzfq04u7l8YJeC75XNweMj7aLzt464k7CIxgAAl2GPPs9yyxhfLovTVBtDAK0EkmB/zFetw/rHn8n7JSmK1UebOs2Pb17utBgLl1ScTQrnvd0Ts2nZCG27WVIc8E+zxA0KraRu/RFJepJSRXybZx1ci97RSncVHB12qeT9XQF1ucQEBAQEBAQEBAQEBAQEBBBdbdWWfS3Tl5u9wA90DQIIK0Mkr3BjG+zU4V7FXfbxmVtNfK4ePep91vd839s91L599fSGWZxyqezg1owHILy/K7W2vT8ZrJI3jpLay+7FvHWrIw51M6VwqmMqbXDs/R21wHcYWGMn6ZvmPdSgLqeGvearu4dcOPk2dAXQxc+9Y/UeHpLYTa2ktN/wBxaWWLG4uibk6cjH5fw83d6pvthfTXLy3V0h8qMlz3mrnnE1J8Tj2rKRpW2dObVI0sDQK0xcaZjILRV2v0n6ajL5d4nbqEJMVmHDJ+b3+46R3qdZ8q7V05XUEBAQEBAQEBAQEGp+p8Rf0nM+jSIpI3mpIOdPDTjisuafi04v2cytakRloo1zalp+PevP3jr1S1qAx7HgeEEc8Dh2LH5aN/6WJOzRudXxSSGhzprIXrcH6R5/L+yWqtmbz/AOqjRD6i3DWDB0cUjwMCSYxyWG86t9eyS2ib5QRRsTKkHLxFZb3C2sy6D6bQyG83O5d8hbCxnPN7j9yer805/iN7XW5xAQEBAQEBAQEBAQEBAQEHOPX7azeem9/M00Nm+CamOQnYDl2FZc36Vrw/vHm3pqKCbcH3UmnzHeBmrINacsfZVec77fl07p2GW1up7mMmrmRRhwpXiTn7VtxasOTbLtfQ9iYNjjuJCXT3n6zy41Iafkb3BdukxHJverYFdV4+9Y7jc3epe+jciTO2VsVoOAt9IMNBwGh1T21XJvnydWuPFC7TbsDmBpJp8xOZwWkUrfbBrIWMPy8aClO/3Kyr0P0jYGw6a2+2czRIIWvlbWvjf43V7ytJOjO90upQICAgICAgICAgIIfrCz+s6Y3GARmV/kOexgOk6mDU33EKu8zKtperjm0s1Qxya3CgFGkD4OXBtOjql6plhOhn4Sc3DEEHHP2LnrVvfSchfskdTiySVp7nnn7V6vB+kcPL+yYqtWbgPrC5jPUJ7m4ufbwEgV+YRk/YAsd+7bTsu7LJoawF1X0rITiMBSi5eTZtpHTfSkuktt1m/A64YxvI6YwSR/rLX1f1rPn7t7XUwEBAQEBAQEBAQEBAQEBAQa96h2br3oTqC2aNTpNvuNLaVq5sZcPiFXeZlW0uLHljoqyjcx8oAxdVorhWoXl4ejXTtssnyMt4iP1HvDQDkC46RXlSq30+nPs7tbQMt7aKBnyRMaxvsaKLvci4g4X/AHLdJOki2/qm3ir5H/SX7mjENeawvNBwdVtTzCx5Z8teO/DlexWzXxMLGhxdg48eeFVWdlq3HZ7et5bMLf8AiR0HEgubXJMor0uuhiICAgICAgICAgICCiaMSQyRkVD2lpB41FEHBNmfSB8ZdR8Ti2ta0ocgvN3uHdrMpRl0yrRWraZns4rBphvPRE3mbO88riQD2UaV6nr/AKOHm/ZsFe5bMXnT1Wu//wCg39walsAZG0Z/LG371zcl6unjnRe224YLRsra/LV9PtquXkba93YPSERnpMysArLdTF5HNtGfY1dfrT8GHP8As3ZbsRAQEBAQEBAQEBAQEBAQEFFxC2eCSF/ySscx3scKFB5T6bsW7dLdba8h89lM+3k01xdE8szHA6V52MV325jcOnpZ371t0D6anXUWtx4fqDDDmp0v5RTbs7+vQcYgwd92e03rZr3artodbXsL4ZARWmoUDh2tOIUWZTLh5TsNtuti3i+2e+obnb5nRSOaaA6ciCeDxQj2rDWfDe9W0bXILOe3umND3QOY8An5jG4Oz7lX5Tjo9CbJvNlvG2w7haOrFKMWn5mPGDmO7WnArply57MM5SgQEBAQEBAQEBAQWb25FrZz3LmlzYI3yFozIY0uoPcg867DLPdRuLR4nOLx2Fzq0Xl7S2vQlkbzY7NE+1YZPnHir3ZLWcUZXkraekmCKwmiAoWzEkDtY0rt4pjVy79042hcATQE4q6ryvvt5vF/1PuU27W81je3N1OPImaWuEQlcyPAjLygKFc3J3dOl6JaJromkF1aYNoKZAYrk5G2juvpPEyPoax0u1F753O7CZn4Lt4P0jm5v2rb1syEBAQEBAQEBAQEBAQEBAQEHly2hkPUm6h9Wzvup3SHEEOMrq4Lhx1rst6RM2DzZ7nBdeYNdvNHIezS4GuKidNsl7PRQIIBBqDiCu9xiAg4161dNWse8We9Q+Ga9jMN0wZuMNCx/t0u0nuWe+vy002+GmWge2EMJB/LhyxqVjZWmXWvSbfBPtsmzSaRLZ1kiLa+ON7quJOVWvctuO9GW8b8tFBAQEBAQEBAQEBAIBBBFQcCCg4R0vYtjv5YHMMUzZXtdE4U0nUQW07CsPDDa7N8ggEcel3yNGZ7FWiS2V7BHOxuOmTV20cwU+xdHH2ZbzqktSuo4/652P8A7xsF83AzCW2e/wDyEPAJ/wBIrHlnWNuO9Ki7TafqYWUNTSpJ5FcfJrl0abYdX9K3aNhntTh5M5IbyD2g/aCun1/1Yc37N0W7IQEBAQEBAQEBAQEBAQEBAQeb91szZepG+WxowfUySNBy0zESt/8AjXJZjaumXOsZF7C1khBx1ChFBl96z27rzs7h0juY3Lpyxua1eIxHJ/nj8Dvfpqu3S5jl2mKmFZUQa91p0pD1Bt2mpbeWwc60dXw6nUqHDt007EHI926Y3jZo2Ov4Pp/N1GIamu1aaax4SaZqljSVj9PbrNtN/BfWzqPidXTWgcw/Mx3YRgqa3C1juez9RbTutjHeWs7dEnhLHOAe14FSxw5hbZY4SLXNcKtIIORGKD6gICAgICAgICAg5j15t52vqm33SABsd8xxkGQ82IAHL8wIVdovKv228CRrfDQuGrGtaHNZVZIbddRx3gZrafNaRpriBXw/HDvWnGpsmfNC1Zuf+txjj6Ni3F7Q5u23sMziTSjXh0bsf9MVVN50X07tZ6W3W13KOKW3eCSzU7kMaU7qLm2nVv1jonQN+2Pe7myqQ2eIPaOGqM/ucr8NU5I6At2QgICAgICAgICAgICAgICBxQefPVOGKH1Vm8Wjz7S3uDTnQxH/AO2FhtrnZtpfxY91uEAiBe35BqGFSRzKptrFpW6elPW+0ROfslw4wS3cxktC6gYSWAFhJOB8OHNacdx0U3ny6utmQgIOaes9x5LNqNcCZ/gGJhMrjkm8RmF5YdTm1q0Z9yxbMeW+kNm6chskbfFpJDXU7CeKi1OIdI9ZXdpdf1HZb2SO4idTyXEljgPma+MnTTgp8kXV6Q6F682zq2xlltmugu7Uhl3avLdQJFdTaE1YTUArTXbLLbXDZlZUQEBAQEBAQEGleq9o12wQX4YXSWNww6hwjk8Lq9+lV2W1aptF06W3jfp0hwwIpgHDJZ5XwkHxW1y9jv8AjwisTxg4H+X7012RY6Rast5LeKVrG0e0OrQcQtss2N1DstrvGxX21XEYfDeQvic2g/EMCK8ilI8ydHTN2iSfb3NME0DjC9gGLXMJBrXlRY+La1tO177cbW62vXXgF4xxcyVra1BwLHMHCmCpNbOsTblvkHqRvbmNe2C2uQaFwYHsIH+s6i0nIr4RuW0dS7Tulv5kMzWSNA86F5Acxx/Ca0r3LWXLOzCTjkjkbqjcHt5tII+CIVICAgICAgICAgICAgIFBWvFBxH+4raHQXWxdQQQSOq91jezsxY1po+HXTKrtQr3Km8+Wmlw5FcdXsFlJ5xIcata8OAIxoM/bxWdrSdTprf/AK6KYXIbHdRSPjLqihoainZkVHdLrXRHrvev3Tb9g3a2beNmlbax7lbv1yuLjpaXxtH4TTU7lir6735Z7aR3FashBRLBBM3TNG2RuIo8BwxzzQcH62/t36gvOrJt46W3CystukcJBt84mbpcW0kaCxr26XGpGGFVndOrScnRrEv9v3rBK2S3im2q3jcCBI65mcMf5RCU8D+n0s2n9tXq3aulki3HaBNOP1HiSdrQaUqGtgTwP6Ouejno/J0RbS3m83jN06jn1Rm8iD2xxwGhETA6lakanOI+xWmsiu29rpisqICAgICAgICCD65j19H7w2laWsrufyt1V7qVRMcBl6nux5FjaAkyERtDTQk8Knksq0Xd53LcLWGK4luvLc6jJHNcQ1rzgKurkTh7VTHVadV6z3vq+ytI5It8uD5J80RmVxjpxYK+HSa4VTzsp4yunbT6w7FJtbJ79svnMb+s+Bge00zdQGo7cMFtptlntph5b9Susd2i9S9+3C0t5Zdsu523No8xuYfKdG0Vy/MCPam2qJthN7P1jZba91zM6MktaYTMaaC4Ygg0WXVtiL8vq3tj9zaIbsMuWNJ1NGqGT8zMBUV7wllTLrY2jberperN/s9r6ZgF1fsj86d0bmhraNLiyRziG5NpmFaSy9FPxx1egekNsvdu2SK3vGNjuC50j42uDg0vx06hgaLa3LFNKAQEBAQEBAQEBAQEBAQYG/bJYb5s91tN+0utbuMxyacHDi1zTjRzTQjtQcavf7S+lLqJ8Tt/3NrX8KWxGdRX9IE49qrNZFrtapj/ALUdmhiMcPUl9E04VbDBUA8Bhgk0ibvWzenX9vnSHQ29s3uyvL693CNj443XMkflt8waXHRGxlTpJGJKnCuXUFKBAQEBAQEBAQEBAQEBAQEHxzWvaWuAc1wo5pxBB4FB5tuvSX1Os7i8kt9uinba3MxsbiKeNrpbcvJj0xk1b4CBpPEKllaTafLTt96L9Vr+88qTpW/ubSdmmZxYGaXVwc06jl7FS6Wr68knRMDp71M2rp02X/je4Xl1I0xMDIPMFSM3ngO5TrojbeZfegPR/wBS9xvrZu42s/TUQa6aa8eGEAgVYxsTZa6tdK5Cgr7YnGm8r1EyybJZxQ34ju5RG1s8jo2hr3ADU7QdQAJxotmCKl6B6Glr5vTu2SVz1WcB+1iBD0B0JASYenNsjLsCW2duK09jEErY7Ztu3x+VYWkNpH+SCNkbfcwBBkDM+1B9QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBxQEBAQEBAQEBB//9k= -------------------------------------------------------------------------------- /test/settings.js: -------------------------------------------------------------------------------- 1 | var indico = require('..') 2 | , settings = require('../lib/settings.js') 3 | , services = require('../lib/services.js') 4 | , should = require('chai').should() 5 | , expandTilde = require('expand-tilde') 6 | , fs = require('fs') 7 | , ini = require('config-ini'); 8 | 9 | describe('Authentication', function() { 10 | describe('Direct config', function() { 11 | it('Should load configuration from a configuration file', function(done) { 12 | var apiKeyArg = "api_key_argument"; 13 | var config = { 14 | 'apiKey': apiKeyArg, 15 | }; 16 | 17 | // directly pass in arguments 18 | var apiKey = settings.resolveApiKey(config); 19 | 20 | apiKey.should.equal(apiKeyArg); 21 | done(); 22 | }); 23 | }); 24 | describe('API Key', function() { 25 | it('Should load configuration from environment variables', function(done) { 26 | var envApiKey = "api_key_env_variable"; 27 | var savedKey = process.env.INDICO_API_KEY; 28 | 29 | // set environment variables 30 | process.env.INDICO_API_KEY = envApiKey; 31 | var config = {}; 32 | 33 | // read from environment variables 34 | var apiKey = settings.resolveApiKey(config); 35 | 36 | apiKey.should.equal(envApiKey) 37 | process.env.INDICO_API_KEY = savedKey; 38 | done(); 39 | }); 40 | }); 41 | describe('API Key', function() { 42 | it('Should load configuration from config file', function(done) { 43 | var apiKeyConfigFile = "api_key_config_file_var"; 44 | 45 | // ensure process does not take precedence 46 | var savedKey = process.env.INDICO_API_KEY; 47 | delete process.env.INDICO_API_KEY; 48 | 49 | config = {}; 50 | 51 | // mock result of indicorc 52 | var configFile = { 53 | 'auth': { 54 | 'api_key': apiKeyConfigFile 55 | } 56 | }; 57 | 58 | var apiKey = settings.resolveApiKey(config, false, configFile); 59 | apiKey.should.equal(apiKeyConfigFile); 60 | process.env.INDICO_API_KEY = savedKey; 61 | done(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('Private Cloud', function() { 67 | describe('Direct config', function() { 68 | it('Should load configuration from a configuration file', function(done) { 69 | var privateCloud = "private_cloud_argument"; 70 | var config = { 71 | "privateCloud": privateCloud 72 | }; 73 | 74 | // directly pass in arguments 75 | var cloud = settings.resolvePrivateCloud(config); 76 | 77 | cloud.should.equal(privateCloud); 78 | done(); 79 | }); 80 | }); 81 | describe('API Key', function() { 82 | it('Should load configuration from environment variables', function(done) { 83 | var privateCloud = "private_cloud_env_variable"; 84 | 85 | // set environment variables 86 | var savedCloud = process.env.INDICO_CLOUD; 87 | process.env.INDICO_CLOUD = privateCloud; 88 | var config = {}; 89 | 90 | // read from environment variables 91 | var cloud = settings.resolvePrivateCloud(config); 92 | 93 | cloud.should.equal(privateCloud); 94 | process.env.INDICO_CLOUD = savedCloud; 95 | done(); 96 | }); 97 | }); 98 | describe('Private Cloud', function() { 99 | it('Should load configuration from config file', function(done) { 100 | var privateCloud = "private_cloud_config_file"; 101 | 102 | // ensure process does not take precedence 103 | delete process.env.INDICO_CLOUD; 104 | 105 | config = {}; 106 | 107 | // mock result of indicorc 108 | var configFile = { 109 | 'private_cloud': { 110 | 'cloud': privateCloud 111 | } 112 | }; 113 | 114 | var cloud = settings.resolvePrivateCloud(config, false, configFile); 115 | cloud.should.equal(privateCloud) 116 | done(); 117 | }); 118 | }); 119 | describe('Private Cloud', function() { 120 | it('Should assign proper priority to module variables for config', function(done) { 121 | var privateCloud = "private_cloud_config_file"; 122 | 123 | config = {}; 124 | 125 | // mock result of indicorc 126 | var configFile = { 127 | 'private_cloud': { 128 | 'cloud': privateCloud 129 | } 130 | }; 131 | 132 | var moduleConfig = 'module_private_cloud'; 133 | var cloud = settings.resolvePrivateCloud(config, moduleConfig, configFile); 134 | cloud.should.equal(moduleConfig); 135 | done(); 136 | }); 137 | }); 138 | describe('API Key', function() { 139 | it('Should assign proper priority to module variables for config', function(done) { 140 | var apiKey = "api_key_config_file"; 141 | 142 | config = {}; 143 | 144 | // mock result of indicorc 145 | var configFile = { 146 | 'auth': { 147 | 'api_key': apiKey 148 | } 149 | }; 150 | 151 | var moduleConfig = 'module_api_key'; 152 | var cloud = settings.resolvePrivateCloud(config, moduleConfig, configFile); 153 | cloud.should.equal(moduleConfig); 154 | done(); 155 | }); 156 | }); 157 | describe('Local Deployment', function() { 158 | it('Should allow easy access to local deploy of indico APIs', function(done) { 159 | indico.host = "localhost:8000" 160 | var url = services.service("/sentiment", null) 161 | url.should.equal("http://localhost:8000/sentiment") 162 | indico.host = "apiv2.indico.io" 163 | done(); 164 | }); 165 | }); 166 | }); 167 | -------------------------------------------------------------------------------- /test/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndicoDataSolutions/IndicoIo-node/80f86c95fe02480b9dedc6935be9477f0c0ea6a4/test/test.pdf --------------------------------------------------------------------------------