From 96694638aa10aaa40eeae4271d1a933baec9fe13 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Fri, 16 Oct 2020 14:46:29 +0200 Subject: [PATCH 01/20] chore: Ignore .mypy_cache y .pytest cache. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b6e4761..b664610 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ share/python-wheels/ *.egg-info/ .installed.cfg *.egg +.mypy_cache +.pytest_cache MANIFEST # PyInstaller From 6c44e16f03ef4286fec3b9fa8941c39b497dd401 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Fri, 16 Oct 2020 14:52:35 +0200 Subject: [PATCH 02/20] chore(chat): empty module --- src/app/chat/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/app/chat/__init__.py diff --git a/src/app/chat/__init__.py b/src/app/chat/__init__.py new file mode 100644 index 0000000..e69de29 From 52fe9868db71cab0cfddb298673b4ddf9c90cee6 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Sat, 17 Oct 2020 14:54:04 +0200 Subject: [PATCH 03/20] feat(basic-chat): small functional demo. --- Pipfile | 4 +- Pipfile.lock | 123 ++++++++++++++++++++++++++++++++++-- src/app/chat.html | 39 ++++++++++++ src/app/chat/__init__.py | 0 src/app/main.py | 29 +++++++++ src/app/static/socket.io.js | 9 +++ 6 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 src/app/chat.html delete mode 100644 src/app/chat/__init__.py create mode 100644 src/app/static/socket.io.js diff --git a/Pipfile b/Pipfile index d717390..6a700c5 100644 --- a/Pipfile +++ b/Pipfile @@ -11,8 +11,10 @@ flake8 = "*" pytest-cov = "*" mypy = "*" requests = "*" -uvicorn = "*" +uvicorn = {extras = ["standard"], version = "*"} flake8-junit-report = "*" +python-socketio = "*" +aiofiles = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 3a4f2da..7a9ea4b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f42db2ac0d3636f99be49d7f223456457f188b7761c60f5ad3e74c5ce6c92936" + "sha256": "74228d22913a3277bf3a1f1c8d1f05fb0d5f207c823bd23a11c6492275a8a7d0" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,14 @@ ] }, "default": { + "aiofiles": { + "hashes": [ + "sha256:377fdf7815cc611870c59cbd07b68b180841d2a2b79812d8c218be02448c2acb", + "sha256:98e6bcfd1b50f97db4980e182ddd509b7cc35909e903a8fe50d8849e02d815af" + ], + "index": "pypi", + "version": "==0.5.0" + }, "attrs": { "hashes": [ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", @@ -116,6 +124,23 @@ ], "version": "==0.11.0" }, + "httptools": { + "hashes": [ + "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", + "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", + "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", + "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", + "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", + "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", + "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", + "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", + "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", + "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", + "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", + "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" + ], + "version": "==0.1.1" + }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", @@ -134,6 +159,7 @@ }, "iniconfig": { "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" ], "version": "==1.1.1" @@ -259,6 +285,44 @@ "index": "pypi", "version": "==2.10.1" }, + "python-dotenv": { + "hashes": [ + "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", + "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" + ], + "version": "==0.14.0" + }, + "python-engineio": { + "hashes": [ + "sha256:36b33c6aa702d9b6a7f527eec6387a2da1a9a24484ec2f086d76576413cef04b", + "sha256:cfded18156862f94544a9f8ef37f56727df731c8552d7023f5afee8369be2db6" + ], + "version": "==3.13.2" + }, + "python-socketio": { + "hashes": [ + "sha256:358d8fbbc029c4538ea25bcaa283e47f375be0017fcba829de8a3a731c9df25a", + "sha256:d437f797c44b6efba2f201867cf02b8c96b97dff26d4e4281ac08b45817cd522" + ], + "index": "pypi", + "version": "==4.6.0" + }, + "pyyaml": { + "hashes": [ + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "version": "==5.3.1" + }, "requests": { "hashes": [ "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", @@ -334,6 +398,9 @@ "version": "==1.25.10" }, "uvicorn": { + "extras": [ + "standard" + ], "hashes": [ "sha256:a461e76406088f448f36323f5ac774d50e5a552b6ccb54e4fca8d83ef614a7c2", "sha256:d06a25caa8dc680ad92eb3ec67363f5281c092059613a1cc0100acba37fc0f45" @@ -341,13 +408,61 @@ "index": "pypi", "version": "==0.12.1" }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "version": "==0.14.0" + }, + "watchgod": { + "hashes": [ + "sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a", + "sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858" + ], + "version": "==0.6" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "version": "==8.1" + }, "zipp": { "hashes": [ - "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b", - "sha256:eed8ec0b8d1416b2ca33516a37a08892442f3954dee131e92cfd92d8fe3e7066" + "sha256:16522f69653f0d67be90e8baa4a46d66389145b734345d68a257da53df670903", + "sha256:c1532a8030c32fd52ff6a288d855fe7adef5823ba1d26a29a68fd6314aa72baa" ], "markers": "python_version >= '3.6'", - "version": "==3.3.0" + "version": "==3.3.1" } }, "develop": {} diff --git a/src/app/chat.html b/src/app/chat.html new file mode 100644 index 0000000..ae075c8 --- /dev/null +++ b/src/app/chat.html @@ -0,0 +1,39 @@ + + + + Socket.IO chat + + + +

Socket.IO chat

+
    +
    + +
    + + + + + diff --git a/src/app/chat/__init__.py b/src/app/chat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/main.py b/src/app/main.py index cc89408..31dffb8 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,6 +1,30 @@ +import socketio + from fastapi import FastAPI +from fastapi.responses import FileResponse, HTMLResponse +from fastapi.staticfiles import StaticFiles app = FastAPI() +app.mount("/static", StaticFiles(directory="static"), name="static") + +sio = socketio.AsyncServer(async_mode='asgi') +app.mount('/sio', socketio.ASGIApp(sio)) + + +@sio.on('connect') +def sio_connect(sid, environ): + print('A user connected') + + +@sio.on('disconnect') +def sio_disconnect(sid): + print('User disconnected') + + +@sio.on('chat message') +async def chat_message(sid, msg): + print('Message: %s' % msg) + await sio.emit('chat message', msg) @app.get("/") @@ -11,3 +35,8 @@ async def home() -> dict: 'message': "Hello world", 'version': "0.1", } + + +@app.get("/chat", response_class=FileResponse) +def chat() -> HTMLResponse: + return FileResponse('chat.html') diff --git a/src/app/static/socket.io.js b/src/app/static/socket.io.js new file mode 100644 index 0000000..270777b --- /dev/null +++ b/src/app/static/socket.io.js @@ -0,0 +1,9 @@ +/*! + * Socket.IO v2.3.0 + * (c) 2014-2019 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){function r(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var n,r=o(t),i=r.source,u=r.id,p=r.path,h=c[u]&&p in c[u].nsps,f=e.forceNew||e["force new connection"]||!1===e.multiplex||h;return f?(a("ignoring socket cache for %s",i),n=s(i,e)):(c[u]||(a("new io instance for %s",i),c[u]=s(i,e)),n=c[u]),r.query&&!e.query&&(e.query=r.query),n.socket(r.path,e)}var o=n(1),i=n(7),s=n(15),a=n(3)("socket.io-client");t.exports=e=r;var c=e.managers={};e.protocol=i.protocol,e.connect=r,e.Manager=n(15),e.Socket=n(39)},function(t,e,n){function r(t,e){var n=t;e=e||"undefined"!=typeof location&&location,null==t&&(t=e.protocol+"//"+e.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?e.protocol+t:e.host+t),/^(https?|wss?):\/\//.test(t)||(i("protocol-less url %s",t),t="undefined"!=typeof e?e.protocol+"//"+t:"https://"+t),i("parse %s",t),n=o(t)),n.port||(/^(http|ws)$/.test(n.protocol)?n.port="80":/^(http|ws)s$/.test(n.protocol)&&(n.port="443")),n.path=n.path||"/";var r=n.host.indexOf(":")!==-1,s=r?"["+n.host+"]":n.host;return n.id=n.protocol+"://"+s+":"+n.port,n.href=n.protocol+"://"+s+(e&&e.port===n.port?"":":"+n.port),n}var o=n(2),i=n(3)("socket.io-client:url");t.exports=r},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");o!=-1&&i!=-1&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s=n.exec(t||""),a={},c=14;c--;)a[r[c]]=s[c]||"";return o!=-1&&i!=-1&&(a.source=e,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a}},function(t,e,n){(function(r){"use strict";function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type&&!window.process.__nwjs)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){if(e[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+e[0]+(this.useColors?"%c ":" ")+"+"+t.exports.humanize(this.diff),this.useColors){var n="color: "+this.color;e.splice(1,0,n,"color: inherit");var r=0,o=0;e[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(r++,"%c"===t&&(o=r))}),e.splice(o,0,n)}}function s(){var t;return"object"===("undefined"==typeof console?"undefined":p(console))&&console.log&&(t=console).log.apply(t,arguments)}function a(t){try{t?e.storage.setItem("debug",t):e.storage.removeItem("debug")}catch(n){}}function c(){var t=void 0;try{t=e.storage.getItem("debug")}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return localStorage}catch(t){}}var p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage=u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.exports=n(5)(e);var h=t.exports.formatters;h.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}}).call(e,n(4))},function(t,e){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function o(t){if(p===setTimeout)return setTimeout(t,0);if((p===n||!p)&&setTimeout)return p=setTimeout,setTimeout(t,0);try{return p(t,0)}catch(e){try{return p.call(null,t,0)}catch(e){return p.call(this,t,0)}}}function i(t){if(h===clearTimeout)return clearTimeout(t);if((h===r||!h)&&clearTimeout)return h=clearTimeout,clearTimeout(t);try{return h(t)}catch(e){try{return h.call(null,t)}catch(e){return h.call(this,t)}}}function s(){y&&l&&(y=!1,l.length?d=l.concat(d):m=-1,d.length&&a())}function a(){if(!y){var t=o(s);y=!0;for(var e=d.length;e;){for(l=d,d=[];++m1)for(var n=1;n100)){var e=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*h;case"weeks":case"week":case"w":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){var e=Math.abs(t);return e>=u?Math.round(t/u)+"d":e>=c?Math.round(t/c)+"h":e>=a?Math.round(t/a)+"m":e>=s?Math.round(t/s)+"s":t+"ms"}function o(t){var e=Math.abs(t);return e>=u?i(t,e,u,"day"):e>=c?i(t,e,c,"hour"):e>=a?i(t,e,a,"minute"):e>=s?i(t,e,s,"second"):t+" ms"}function i(t,e,n,r){var o=e>=1.5*n;return Math.round(t/n)+" "+r+(o?"s":"")}var s=1e3,a=60*s,c=60*a,u=24*c,p=7*u,h=365.25*u;t.exports=function(t,e){e=e||{};var i=typeof t;if("string"===i&&t.length>0)return n(t);if("number"===i&&isFinite(t))return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(){}function o(t){var n=""+t.type;if(e.BINARY_EVENT!==t.type&&e.BINARY_ACK!==t.type||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data){var r=i(t.data);if(r===!1)return g;n+=r}return f("encoded %j as %s",t,n),n}function i(t){try{return JSON.stringify(t)}catch(e){return!1}}function s(t,e){function n(t){var n=d.deconstructPacket(t),r=o(n.packet),i=n.buffers;i.unshift(r),e(i)}d.removeBlobs(t,n)}function a(){this.reconstructor=null}function c(t){var n=0,r={type:Number(t.charAt(0))};if(null==e.types[r.type])return h("unknown packet type "+r.type);if(e.BINARY_EVENT===r.type||e.BINARY_ACK===r.type){for(var o="";"-"!==t.charAt(++n)&&(o+=t.charAt(n),n!=t.length););if(o!=Number(o)||"-"!==t.charAt(n))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(n+1))for(r.nsp="";++n;){var i=t.charAt(n);if(","===i)break;if(r.nsp+=i,n===t.length)break}else r.nsp="/";var s=t.charAt(n+1);if(""!==s&&Number(s)==s){for(r.id="";++n;){var i=t.charAt(n);if(null==i||Number(i)!=i){--n;break}if(r.id+=t.charAt(n),n===t.length)break}r.id=Number(r.id)}if(t.charAt(++n)){var a=u(t.substr(n)),c=a!==!1&&(r.type===e.ERROR||y(a));if(!c)return h("invalid payload");r.data=a}return f("decoded %s as %j",t,r),r}function u(t){try{return JSON.parse(t)}catch(e){return!1}}function p(t){this.reconPack=t,this.buffers=[]}function h(t){return{type:e.ERROR,data:"parser error: "+t}}var f=n(8)("socket.io-parser"),l=n(11),d=n(12),y=n(13),m=n(14);e.protocol=4,e.types=["CONNECT","DISCONNECT","EVENT","ACK","ERROR","BINARY_EVENT","BINARY_ACK"],e.CONNECT=0,e.DISCONNECT=1,e.EVENT=2,e.ACK=3,e.ERROR=4,e.BINARY_EVENT=5,e.BINARY_ACK=6,e.Encoder=r,e.Decoder=a;var g=e.ERROR+'"encode error"';r.prototype.encode=function(t,n){if(f("encoding packet %j",t),e.BINARY_EVENT===t.type||e.BINARY_ACK===t.type)s(t,n);else{var r=o(t);n([r])}},l(a.prototype),a.prototype.add=function(t){var n;if("string"==typeof t)n=c(t),e.BINARY_EVENT===n.type||e.BINARY_ACK===n.type?(this.reconstructor=new p(n),0===this.reconstructor.reconPack.attachments&&this.emit("decoded",n)):this.emit("decoded",n);else{if(!m(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");n=this.reconstructor.takeBinaryData(t),n&&(this.reconstructor=null,this.emit("decoded",n))}},a.prototype.destroy=function(){this.reconstructor&&this.reconstructor.finishedReconstruction()},p.prototype.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=d.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null},p.prototype.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]}},function(t,e,n){(function(r){"use strict";function o(){return!("undefined"==typeof window||!window.process||"renderer"!==window.process.type)||("undefined"==typeof navigator||!navigator.userAgent||!navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))&&("undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(t){var n=this.useColors;if(t[0]=(n?"%c":"")+this.namespace+(n?" %c":" ")+t[0]+(n?"%c ":" ")+"+"+e.humanize(this.diff),n){var r="color: "+this.color;t.splice(1,0,r,"color: inherit");var o=0,i=0;t[0].replace(/%[a-zA-Z%]/g,function(t){"%%"!==t&&(o++,"%c"===t&&(i=o))}),t.splice(i,0,r)}}function s(){return"object"===("undefined"==typeof console?"undefined":p(console))&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{null==t?e.storage.removeItem("debug"):e.storage.debug=t}catch(n){}}function c(){var t;try{t=e.storage.debug}catch(n){}return!t&&"undefined"!=typeof r&&"env"in r&&(t=r.env.DEBUG),t}function u(){try{return window.localStorage}catch(t){}}var p="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e=t.exports=n(9),e.log=s,e.formatArgs=i,e.save=a,e.load=c,e.useColors=o,e.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),e.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],e.formatters.j=function(t){try{return JSON.stringify(t)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}},e.enable(c())}).call(e,n(4))},function(t,e,n){"use strict";function r(t){var n,r=0;for(n in t)r=(r<<5)-r+t.charCodeAt(n),r|=0;return e.colors[Math.abs(r)%e.colors.length]}function o(t){function n(){if(n.enabled){var t=n,r=+new Date,i=r-(o||r);t.diff=i,t.prev=o,t.curr=r,o=r;for(var s=new Array(arguments.length),a=0;a100)){var e=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"yrs":case"yr":case"y":return n*p;case"days":case"day":case"d":return n*u;case"hours":case"hour":case"hrs":case"hr":case"h":return n*c;case"minutes":case"minute":case"mins":case"min":case"m":return n*a;case"seconds":case"second":case"secs":case"sec":case"s":return n*s;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return n;default:return}}}}function r(t){return t>=u?Math.round(t/u)+"d":t>=c?Math.round(t/c)+"h":t>=a?Math.round(t/a)+"m":t>=s?Math.round(t/s)+"s":t+"ms"}function o(t){return i(t,u,"day")||i(t,c,"hour")||i(t,a,"minute")||i(t,s,"second")||t+" ms"}function i(t,e,n){if(!(t0)return n(t);if("number"===i&&isNaN(t)===!1)return e["long"]?o(t):r(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))}},function(t,e,n){function r(t){if(t)return o(t)}function o(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments)}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n=this._callbacks["$"+t];if(!n)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,o=0;o0&&!this.encoding){var t=this.packetBuffer.shift();this.packet(t)}},r.prototype.cleanup=function(){p("cleanup");for(var t=this.subs.length,e=0;e=this._reconnectionAttempts)p("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();p("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(p("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(p("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(p("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(17),t.exports.parser=n(24)},function(t,e,n){function r(t,e){return this instanceof r?(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t?(t=p(t),e.hostname=t.host,e.secure="https"===t.protocol||"wss"===t.protocol,e.port=t.port,t.query&&(e.query=t.query)):e.host&&(e.hostname=p(e.host).host),this.secure=null!=e.secure?e.secure:"undefined"!=typeof location&&"https:"===location.protocol,e.hostname&&!e.port&&(e.port=this.secure?"443":"80"),this.agent=e.agent||!1,this.hostname=e.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=e.port||("undefined"!=typeof location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.withCredentials=!1!==e.withCredentials,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.transportOptions=e.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.perMessageDeflate=!1!==e.perMessageDeflate&&(e.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=void 0===e.rejectUnauthorized||e.rejectUnauthorized,this.forceNode=!!e.forceNode,this.isReactNative="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),("undefined"==typeof self||this.isReactNative)&&(e.extraHeaders&&Object.keys(e.extraHeaders).length>0&&(this.extraHeaders=e.extraHeaders),e.localAddress&&(this.localAddress=e.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,void this.open()):new r(t,e)}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(18),s=n(11),a=n(3)("engine.io-client:socket"),c=n(38),u=n(24),p=n(2),h=n(32);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=u.protocol,r.Socket=r,r.Transport=n(23),r.transports=n(18),r.parser=n(24),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=u.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,withCredentials:n.withCredentials||this.withCredentials,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0,isReactNative:this.isReactNative});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{ +if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),p.send([{type:"ping",data:"probe"}]),p.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",p),!p)return;r.priorWebsocketSuccess="websocket"===p.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),u(),f.setTransport(p),p.send([{type:"upgrade"}]),f.emit("upgrade",p),p=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=p.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,u(),p.close(),p=null)}function o(e){var r=new Error("probe error: "+e);r.transport=p.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){p&&t.name!==p.name&&(a('"%s" works - aborting "%s"',t.name,p.name),n())}function u(){p.removeListener("open",e),p.removeListener("error",o),p.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var p=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,p.once("open",e),p.once("error",o),p.once("close",i),this.once("close",s),this.once("upgrading",c),p.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:C}var i=new Uint8Array(t),o=i[0],s=f(t,1);return w&&"blob"===n&&(s=new w([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!u)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=u.decode(t.substr(1));return"blob"===e&&w&&(r=new w([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?w&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(C,0,1);for(var i,s,a="",c=0,u=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",u=1;255!==s[u];u++){if(c.length>310)return r(C,0,1);c+=s[u]}o=f(o,2+c.length),c=parseInt(c);var p=f(o,0,c);if(a)try{p=String.fromCharCode.apply(null,new Uint8Array(p))}catch(h){var l=new Uint8Array(p);p="";for(var u=0;ur&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=d(e>>>10&1023|55296),e=56320|1023&e),o+=d(e);return o}function o(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function i(t,e){return d(t>>e&63|128)}function s(t,e){if(0==(4294967168&t))return d(t);var n="";return 0==(4294965248&t)?n=d(t>>6&31|192):0==(4294901760&t)?(o(t,e)||(t=65533),n=d(t>>12&15|224),n+=i(t,6)):0==(4292870144&t)&&(n=d(t>>18&7|240),n+=i(t,12),n+=i(t,6)),n+=d(63&t|128)}function a(t,e){e=e||{};for(var r,o=!1!==e.strict,i=n(t),a=i.length,c=-1,u="";++c=f)throw Error("Invalid byte index");var t=255&h[l];if(l++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function u(t){var e,n,r,i,s;if(l>f)throw Error("Invalid byte index");if(l==f)return!1;if(e=255&h[l],l++,0==(128&e))return e;if(192==(224&e)){if(n=c(),s=(31&e)<<6|n,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=c(),r=c(),s=(15&e)<<12|n<<6|r,s>=2048)return o(s,t)?s:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=c(),r=c(),i=c(),s=(7&e)<<18|n<<12|r<<6|i,s>=65536&&s<=1114111))return s;throw Error("Invalid UTF-8 detected")}function p(t,e){e=e||{};var o=!1!==e.strict;h=n(t),f=h.length,l=0;for(var i,s=[];(i=u(o))!==!1;)s.push(i);return r(s)}/*! https://mths.be/utf8js v2.1.2 by @mathias */ +var h,f,l,d=String.fromCharCode;t.exports={version:"2.1.2",encode:a,decode:p}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,u=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var p=new ArrayBuffer(a),h=new Uint8Array(p);for(e=0;e>4,h[u++]=(15&o)<<4|i>>2,h[u++]=(3&i)<<6|63&s;return p}}()},function(t,e){function n(t){return t.map(function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var n=new Uint8Array(t.byteLength);n.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=n.buffer}return e}return t})}function r(t,e){e=e||{};var r=new i;return n(t).forEach(function(t){r.append(t)}),e.type?r.getBlob(e.type):r.getBlob()}function o(t,e){return new Blob(n(t),e||{})}var i="undefined"!=typeof i?i:"undefined"!=typeof WebKitBlobBuilder?WebKitBlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder&&MozBlobBuilder,s=function(){try{var t=new Blob(["hi"]);return 2===t.size}catch(e){return!1}}(),a=s&&function(){try{var t=new Blob([new Uint8Array([1,2])]);return 2===t.size}catch(e){return!1}}(),c=i&&i.prototype.append&&i.prototype.getBlob;"undefined"!=typeof Blob&&(r.prototype=Blob.prototype,o.prototype=Blob.prototype),t.exports=function(){return s?a?Blob:o:c?r:void 0}()},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r0);return e}function r(t){var e=0;for(p=0;p';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),c=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=c,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(p,"\\\n"),this.area.value=t.replace(u,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=o&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=i),s.call(this,t)}var o,i,s=n(23),a=n(24),c=n(32),u=n(33),p=n(34),h=n(3)("engine.io-client:websocket");if("undefined"!=typeof WebSocket?o=WebSocket:"undefined"!=typeof self&&(o=self.WebSocket||self.MozWebSocket),"undefined"==typeof window)try{i=n(37)}catch(f){}var l=o||i;t.exports=r,u(r,s),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket&&!this.isReactNative?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function e(){n.emit("flush"),setTimeout(function(){n.writable=!0,n.emit("drain")},0)}var n=this;this.writable=!1;for(var r=t.length,o=0,i=r;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])}); +//# sourceMappingURL=socket.io.js.map \ No newline at end of file From 8bf89d78e9a271168f6c57396de29ce56f8bc038 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Mon, 19 Oct 2020 22:09:40 +0200 Subject: [PATCH 04/20] chore(chat): docstrings, correct fileresponse. --- src/app/main.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/app/main.py b/src/app/main.py index 31dffb8..fb03d97 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,28 +1,34 @@ +"""The FastAPI main module""" +import os import socketio from fastapi import FastAPI -from fastapi.responses import FileResponse, HTMLResponse +from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles app = FastAPI() -app.mount("/static", StaticFiles(directory="static"), name="static") +path = os.path.dirname(__file__) +app.mount("/static", StaticFiles(directory=os.path.join(path, "static")), name="static") sio = socketio.AsyncServer(async_mode='asgi') -app.mount('/sio', socketio.ASGIApp(sio)) +app.mount('/sio', socketio.ASGIApp(sio)) # socketio adds automatically /socket.io/ to the URL. @sio.on('connect') def sio_connect(sid, environ): + """A user / browser tab has connected""" print('A user connected') @sio.on('disconnect') def sio_disconnect(sid): + """A user / browser tab has disconnected""" print('User disconnected') @sio.on('chat message') async def chat_message(sid, msg): + """A chat message has arrived""" print('Message: %s' % msg) await sio.emit('chat message', msg) @@ -38,5 +44,5 @@ async def home() -> dict: @app.get("/chat", response_class=FileResponse) -def chat() -> HTMLResponse: +def chat() -> FileResponse: return FileResponse('chat.html') From 68ed3a4a0949ac94facd701f208b56d91b9eccc9 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Mon, 19 Oct 2020 22:10:59 +0200 Subject: [PATCH 05/20] chore(chat): tests with python-socketio (WIP). --- src/app/tests/test_chat.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/app/tests/test_chat.py diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py new file mode 100644 index 0000000..2337531 --- /dev/null +++ b/src/app/tests/test_chat.py @@ -0,0 +1,45 @@ +import asyncio +import pytest +import socketio +import uvicorn + +from ..main import app + +PORT = 5000 + +server = None +server_task = None + + +def get_server(): + config = uvicorn.Config(app, host='localhost', port=PORT) + server = uvicorn.Server(config=config) + config.setup_event_loop() + return server + + +@pytest.fixture +async def async_get_server(): + global server + global server_task + print("Starting server") + server = get_server() + server_task = server.serve() + asyncio.ensure_future(server_task) + asyncio.sleep(1) + + +@pytest.mark.asyncio +async def test_websocket(async_get_server): + sio = socketio.AsyncClient() + + @sio.on('chat message') + def message(data): + print(f"Client received: {data}") + + await sio.connect(f'http://localhost:{PORT}', socketio_path='/sio/socket.io/') + await sio.emit('chat message', 'HOLA!') + await sio.disconnect() + server.should_exit = True + await server_task.close() + asyncio.get_running_loop().stop() From 716a98469f31f50d2f4c3646528f9dbb9da3548d Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Mon, 19 Oct 2020 22:12:44 +0200 Subject: [PATCH 06/20] test(chat): additional dependencies for chat tests. --- Pipfile | 4 +- Pipfile.lock | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 200 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 6a700c5..91d2fd1 100644 --- a/Pipfile +++ b/Pipfile @@ -13,8 +13,10 @@ mypy = "*" requests = "*" uvicorn = {extras = ["standard"], version = "*"} flake8-junit-report = "*" -python-socketio = "*" aiofiles = "*" +ipdb = "*" +python-socketio = {extras = ["asyncio_client"], version = "*"} +pytest-asyncio = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 7a9ea4b..428eb78 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "74228d22913a3277bf3a1f1c8d1f05fb0d5f207c823bd23a11c6492275a8a7d0" + "sha256": "5520b3ab6976402d17ec45319b5067ecf7e137a30a06b1e5100bcf09b7e0d207" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,40 @@ "index": "pypi", "version": "==0.5.0" }, + "aiohttp": { + "hashes": [ + "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", + "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599", + "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8", + "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a", + "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37", + "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5", + "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0", + "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c", + "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645", + "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98", + "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d", + "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81", + "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5" + ], + "version": "==3.6.3" + }, + "appnope": { + "hashes": [ + "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", + "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.0" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, "attrs": { "hashes": [ "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", @@ -32,6 +66,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.2.0" }, + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, "certifi": { "hashes": [ "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", @@ -94,6 +135,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.3" }, + "decorator": { + "hashes": [ + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" + ], + "version": "==4.4.2" + }, "fastapi": { "hashes": [ "sha256:61ed73b4304413a2ea618d1b95ea866ee386e0e62dd8659c4f5059286f4a39c2", @@ -164,6 +212,36 @@ ], "version": "==1.1.1" }, + "ipdb": { + "hashes": [ + "sha256:c85398b5fb82f82399fc38c44fe3532c0dde1754abee727d8f5cfcc74547b334" + ], + "index": "pypi", + "version": "==0.13.4" + }, + "ipython": { + "hashes": [ + "sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8", + "sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e" + ], + "markers": "python_version >= '3.7'", + "version": "==7.18.1" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "jedi": { + "hashes": [ + "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20", + "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.2" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -171,6 +249,29 @@ ], "version": "==0.6.1" }, + "multidict": { + "hashes": [ + "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a", + "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000", + "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2", + "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507", + "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5", + "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7", + "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d", + "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463", + "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19", + "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3", + "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b", + "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c", + "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87", + "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7", + "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430", + "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255", + "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d" + ], + "markers": "python_version >= '3.5'", + "version": "==4.7.6" + }, "mypy": { "hashes": [ "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324", @@ -206,6 +307,29 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4" }, + "parso": { + "hashes": [ + "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea", + "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.7.1" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.8.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", @@ -214,6 +338,21 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, + "prompt-toolkit": { + "hashes": [ + "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c", + "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.8" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, "py": { "hashes": [ "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", @@ -261,6 +400,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, + "pygments": { + "hashes": [ + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" + ], + "markers": "python_version >= '3.5'", + "version": "==2.7.1" + }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", @@ -277,6 +424,14 @@ "markers": "python_version >= '3.5'", "version": "==6.1.1" }, + "pytest-asyncio": { + "hashes": [ + "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", + "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" + ], + "index": "pypi", + "version": "==0.14.0" + }, "pytest-cov": { "hashes": [ "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191", @@ -300,6 +455,9 @@ "version": "==3.13.2" }, "python-socketio": { + "extras": [ + "asyncio_client" + ], "hashes": [ "sha256:358d8fbbc029c4538ea25bcaa283e47f375be0017fcba829de8a3a731c9df25a", "sha256:d437f797c44b6efba2f201867cf02b8c96b97dff26d4e4281ac08b45817cd522" @@ -354,6 +512,14 @@ ], "version": "==0.10.1" }, + "traitlets": { + "hashes": [ + "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", + "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0.5" + }, "typed-ast": { "hashes": [ "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", @@ -429,6 +595,13 @@ ], "version": "==0.6" }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + }, "websockets": { "hashes": [ "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", @@ -456,6 +629,29 @@ ], "version": "==8.1" }, + "yarl": { + "hashes": [ + "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409", + "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593", + "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2", + "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8", + "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d", + "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692", + "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02", + "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a", + "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8", + "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6", + "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511", + "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e", + "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a", + "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb", + "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f", + "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317", + "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.5.1" + }, "zipp": { "hashes": [ "sha256:16522f69653f0d67be90e8baa4a46d66389145b734345d68a257da53df670903", From 58bcd909c1a473d50f43d22a9b87960bbb3f6f18 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 19:07:28 +0200 Subject: [PATCH 07/20] test(chat): basic unit tests --- src/app/tests/test_chat.py | 58 +++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index 2337531..b19e1ce 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -3,43 +3,67 @@ import socketio import uvicorn -from ..main import app +from .. import main PORT = 5000 -server = None -server_task = None - def get_server(): - config = uvicorn.Config(app, host='localhost', port=PORT) + """Get a complete server instance""" + config = uvicorn.Config(main.app, host='127.0.0.1', port=PORT) server = uvicorn.Server(config=config) + # deactivate monitoring task to avoid errores during shutdown + main.sio.eio.start_service_task = False config.setup_event_loop() return server +async def wait_ready(server, interval=0.05, max_wait=5): + """Wait for the server to be ready""" + i = 0 + while not server.started: + await asyncio.sleep(interval) + i += interval + if i > max_wait: + raise RuntimeError(f"Server couldn't startup in {max_wait} seconds") + + @pytest.fixture async def async_get_server(): - global server - global server_task - print("Starting server") + """Start server as test fixture and tear down after test""" server = get_server() - server_task = server.serve() - asyncio.ensure_future(server_task) - asyncio.sleep(1) + serve_task = asyncio.create_task(server.serve()) + await wait_ready(server) + yield + # teardown code + server.should_exit = True + await serve_task # allow server run tasks before shut down @pytest.mark.asyncio -async def test_websocket(async_get_server): +async def test_chat_simple(async_get_server): + """A simple websocket test""" + + class Result: + """Generic message result""" + # add any attributes you need + message_received = False + message = None + sio = socketio.AsyncClient() + result = Result() @sio.on('chat message') - def message(data): + def on_message_received(data): print(f"Client received: {data}") + result.message_received = True + result.message = data + message = 'Hello!' await sio.connect(f'http://localhost:{PORT}', socketio_path='/sio/socket.io/') - await sio.emit('chat message', 'HOLA!') + print(f"Client sends: {message}") + await sio.emit('chat message', message) + await sio.sleep(0.1) await sio.disconnect() - server.should_exit = True - await server_task.close() - asyncio.get_running_loop().stop() + assert result.message_received is True + assert result.message == message From 027612e6c0e0cd10560f9ea0ef76a5d20cf331cb Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 19:31:40 +0200 Subject: [PATCH 08/20] test(generic): Flake8 rules --- .flake8 | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c013a79 --- /dev/null +++ b/.flake8 @@ -0,0 +1,9 @@ +[flake8] +max-line-length = 119 +max-complexity = 10 +ignore = + +# darglint +# Allow one line docstrings without arg spec +strictness=short +docstring_style=google From 29a3fc2526fd6c207fb20340529d80454336234c Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 19:44:43 +0200 Subject: [PATCH 09/20] test(mypy): ignore socketio and uvicorn The have no type annotations. --- .mypy.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .mypy.ini diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..680c733 --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,8 @@ +[mypy] + +[mypy-socketio.*] +ignore_missing_imports = True + +[mypy-uvicorn.*] +ignore_missing_imports = True + From 1170d5929bef88213e7a78e70ed857351f68c23c Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 20:08:46 +0200 Subject: [PATCH 10/20] feat(basic-app): use relative page path for chat.html --- src/app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/main.py b/src/app/main.py index fb03d97..e35a10c 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -45,4 +45,4 @@ async def home() -> dict: @app.get("/chat", response_class=FileResponse) def chat() -> FileResponse: - return FileResponse('chat.html') + return FileResponse(os.path.join(os.path.dirname(__file__), 'chat.html')) From a4150b233377a5c2e1758f7cec51f55106f6f6ca Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 20:10:05 +0200 Subject: [PATCH 11/20] test(cov): ignore line --- src/app/tests/test_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index b19e1ce..b82b3e9 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -25,7 +25,7 @@ async def wait_ready(server, interval=0.05, max_wait=5): await asyncio.sleep(interval) i += interval if i > max_wait: - raise RuntimeError(f"Server couldn't startup in {max_wait} seconds") + raise RuntimeError(f"Server couldn't startup in {max_wait} seconds") # pragma: no cover @pytest.fixture From 4d8788f09f2127afb90e0e47798592f7001a057a Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 20:10:40 +0200 Subject: [PATCH 12/20] Test the chat page. --- src/app/tests/test_chat.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index b82b3e9..33e3036 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -1,12 +1,22 @@ +# stdlib imports import asyncio +import os + +# 3rd party imports import pytest import socketio import uvicorn +# fastapi imports +from fastapi.testclient import TestClient + +# project imports from .. import main PORT = 5000 +client = TestClient(main.app) + def get_server(): """Get a complete server instance""" @@ -67,3 +77,13 @@ def on_message_received(data): await sio.disconnect() assert result.message_received is True assert result.message == message + + +def test_chat_page(): + """Check if chat page returns contents""" + response = client.get("/chat") + assert response.status_code == 200 + fn = os.path.join(os.path.dirname(__file__), '..', 'chat.html') + print(f"Chat page: {fn}") + with open(fn, 'rb') as page: + assert response.content == page.read() From c98fbf95b86902a2b7411f687546311d1d6579cb Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 20:11:17 +0200 Subject: [PATCH 13/20] pep8 --- src/app/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/main.py b/src/app/main.py index e35a10c..00f58aa 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -11,7 +11,8 @@ app.mount("/static", StaticFiles(directory=os.path.join(path, "static")), name="static") sio = socketio.AsyncServer(async_mode='asgi') -app.mount('/sio', socketio.ASGIApp(sio)) # socketio adds automatically /socket.io/ to the URL. +# socketio adds automatically /socket.io/ to the URL. +app.mount('/sio', socketio.ASGIApp(sio)) @sio.on('connect') From 2f4b65be228061f16ff69c1c6599f4a7a0ab6387 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 20:11:33 +0200 Subject: [PATCH 14/20] doc: improve docstrings. --- src/app/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/main.py b/src/app/main.py index 00f58aa..2314635 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -17,20 +17,20 @@ @sio.on('connect') def sio_connect(sid, environ): - """A user / browser tab has connected""" + """Track user connection""" print('A user connected') @sio.on('disconnect') def sio_disconnect(sid): - """A user / browser tab has disconnected""" + """Track user disconnection""" print('User disconnected') @sio.on('chat message') async def chat_message(sid, msg): - """A chat message has arrived""" - print('Message: %s' % msg) + """Receive a chat message and send to all clients""" + print(f"Server received and sends to all clients: {msg}") await sio.emit('chat message', msg) From 8b104637449d524ec227ffd54cdb7b249ef5ad40 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Tue, 20 Oct 2020 20:12:26 +0200 Subject: [PATCH 15/20] test(flake8): Send output to stdout too --- run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-tests.sh b/run-tests.sh index e843ec6..924afca 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -18,7 +18,7 @@ function msgsuccess(){ # run first syntax and code style checks msgrun flake8 -$RUNNER run flake8 --output-file $DIR/flake8.txt src +$RUNNER run flake8 src 2>&1 | tee $DIR/flake8.txt msgsuccess $? flake8 $RUNNER run flake8_junit $DIR/flake8.txt $DIR/flake8_junit.xml >/dev/null From 86a43e58d22a1ed9b6caf3c9a2bcec012b073463 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Wed, 21 Oct 2020 01:22:31 +0200 Subject: [PATCH 16/20] tests(runner): fix flake8 exit status Run flake8 with --tee, using tee alway returns exit status 0. --- run-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-tests.sh b/run-tests.sh index 924afca..624ebba 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -18,7 +18,7 @@ function msgsuccess(){ # run first syntax and code style checks msgrun flake8 -$RUNNER run flake8 src 2>&1 | tee $DIR/flake8.txt +$RUNNER run flake8 --tee --output-file $DIR/flake8.txt msgsuccess $? flake8 $RUNNER run flake8_junit $DIR/flake8.txt $DIR/flake8_junit.xml >/dev/null From be3287bf5b4b58d7d9ad3e6a7a7fe145e6f52d67 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Wed, 21 Oct 2020 02:13:26 +0200 Subject: [PATCH 17/20] test(uvicorn): special uvicorn test server --- src/app/tests/test_chat.py | 90 ++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index 33e3036..7b430cb 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -1,3 +1,4 @@ +from typing import List, Optional # stdlib imports import asyncio import os @@ -7,76 +8,89 @@ import socketio import uvicorn -# fastapi imports +# FastAPI imports +from fastapi import FastAPI from fastapi.testclient import TestClient # project imports from .. import main -PORT = 5000 +PORT = 8000 +# deactivate monitoring task in python-socketio to avoid errores during shutdown +main.sio.eio.start_service_task = False client = TestClient(main.app) -def get_server(): - """Get a complete server instance""" - config = uvicorn.Config(main.app, host='127.0.0.1', port=PORT) - server = uvicorn.Server(config=config) - # deactivate monitoring task to avoid errores during shutdown - main.sio.eio.start_service_task = False - config.setup_event_loop() - return server +class UvicornTestServer(uvicorn.Server): + """Uvicorn test server + Usage: + @pytest.fixture + server = UvicornTestServer() + await server.up() + yield + await server.down() + """ -async def wait_ready(server, interval=0.05, max_wait=5): - """Wait for the server to be ready""" - i = 0 - while not server.started: - await asyncio.sleep(interval) - i += interval - if i > max_wait: - raise RuntimeError(f"Server couldn't startup in {max_wait} seconds") # pragma: no cover + def __init__(self, app: FastAPI = main.app, host: str = '127.0.0.1', port: int = PORT): + """Create a Uvicorn test server + + Args: + app (FastAPI, optional): the FastAPI app. Defaults to main.app. + host (str, optional): the host ip. Defaults to '127.0.0.1'. + port (int, optional): the port. Defaults to PORT. + """ + self._startup_done = asyncio.Event() + super().__init__(config=uvicorn.Config(app, host=host, port=port)) + + async def startup(self, sockets: Optional[List] = None) -> None: + """Override uvicorn startup""" + await super().startup(sockets=sockets) + self.config.setup_event_loop() + self._startup_done.set() + + async def up(self) -> None: + """Start up server asynchronously""" + self._serve_task = asyncio.create_task(self.serve()) + await self._startup_done.wait() + + async def down(self) -> None: + """Shut down server asynchronously""" + self.should_exit = True + await self._serve_task @pytest.fixture -async def async_get_server(): +async def startup_and_shutdown_server(): """Start server as test fixture and tear down after test""" - server = get_server() - serve_task = asyncio.create_task(server.serve()) - await wait_ready(server) + server = UvicornTestServer() + await server.up() yield - # teardown code - server.should_exit = True - await serve_task # allow server run tasks before shut down + await server.down() @pytest.mark.asyncio -async def test_chat_simple(async_get_server): +async def test_chat_simple(startup_and_shutdown_server): """A simple websocket test""" - class Result: - """Generic message result""" - # add any attributes you need - message_received = False - message = None - sio = socketio.AsyncClient() - result = Result() + future = asyncio.get_running_loop().create_future() @sio.on('chat message') def on_message_received(data): print(f"Client received: {data}") - result.message_received = True - result.message = data + # set the result + future.set_result(message) message = 'Hello!' await sio.connect(f'http://localhost:{PORT}', socketio_path='/sio/socket.io/') print(f"Client sends: {message}") await sio.emit('chat message', message) - await sio.sleep(0.1) + # wait for the result to be set (avoid waiting forever) + await asyncio.wait_for(future, timeout=1.0) await sio.disconnect() - assert result.message_received is True - assert result.message == message + assert future.result() == message def test_chat_page(): From 4d3675149695e3564d8a3ce5a942a895ac566a57 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Wed, 21 Oct 2020 02:20:06 +0200 Subject: [PATCH 18/20] test(minor): minor improvement --- src/app/tests/test_chat.py | 2 +- src/app/tests/test_main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index 7b430cb..4f35d67 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -96,7 +96,7 @@ def on_message_received(data): def test_chat_page(): """Check if chat page returns contents""" response = client.get("/chat") - assert response.status_code == 200 + assert response.ok fn = os.path.join(os.path.dirname(__file__), '..', 'chat.html') print(f"Chat page: {fn}") with open(fn, 'rb') as page: diff --git a/src/app/tests/test_main.py b/src/app/tests/test_main.py index 2eabd9d..b7bf4f6 100644 --- a/src/app/tests/test_main.py +++ b/src/app/tests/test_main.py @@ -7,7 +7,7 @@ def test_home(): """Test home view""" response = client.get("/") - assert response.status_code == 200 + assert response.ok result = response.json() assert 'message' in result assert result['message'].lower().startswith("hello world") From 6a5d079198ed62de91adb7ad3fa370c627883f12 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Wed, 21 Oct 2020 02:28:07 +0200 Subject: [PATCH 19/20] test(chat): small fix --- src/app/tests/test_chat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index 4f35d67..801a9a6 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -1,3 +1,4 @@ +"""Some python-socketio tests""" from typing import List, Optional # stdlib imports import asyncio @@ -81,7 +82,7 @@ async def test_chat_simple(startup_and_shutdown_server): def on_message_received(data): print(f"Client received: {data}") # set the result - future.set_result(message) + future.set_result(data) message = 'Hello!' await sio.connect(f'http://localhost:{PORT}', socketio_path='/sio/socket.io/') From 562ba77d092d986894c6557dd206d651b8375670 Mon Sep 17 00:00:00 2001 From: Ernesto Revilla Date: Wed, 21 Oct 2020 02:53:18 +0200 Subject: [PATCH 20/20] doc(pylint): improve --- src/app/main.py | 7 ++++--- src/app/tests/test_chat.py | 28 +++++++++++++++------------- src/app/tests/test_main.py | 2 ++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/app/main.py b/src/app/main.py index 2314635..66f06a7 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -16,19 +16,19 @@ @sio.on('connect') -def sio_connect(sid, environ): +def sio_connect(sid, environ): # pylint: disable=unused-argument """Track user connection""" print('A user connected') @sio.on('disconnect') -def sio_disconnect(sid): +def sio_disconnect(sid): # pylint: disable=unused-argument """Track user disconnection""" print('User disconnected') @sio.on('chat message') -async def chat_message(sid, msg): +async def chat_message(sid, msg): # pylint: disable=unused-argument """Receive a chat message and send to all clients""" print(f"Server received and sends to all clients: {msg}") await sio.emit('chat message', msg) @@ -46,4 +46,5 @@ async def home() -> dict: @app.get("/chat", response_class=FileResponse) def chat() -> FileResponse: + """Load the chat html page""" return FileResponse(os.path.join(os.path.dirname(__file__), 'chat.html')) diff --git a/src/app/tests/test_chat.py b/src/app/tests/test_chat.py index 801a9a6..f7ff307 100644 --- a/src/app/tests/test_chat.py +++ b/src/app/tests/test_chat.py @@ -1,5 +1,5 @@ """Some python-socketio tests""" -from typing import List, Optional +from typing import Any, List, Optional, Awaitable # stdlib imports import asyncio import os @@ -13,7 +13,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -# project imports +# project importsn from .. import main PORT = 8000 @@ -43,6 +43,7 @@ def __init__(self, app: FastAPI = main.app, host: str = '127.0.0.1', port: int = port (int, optional): the port. Defaults to PORT. """ self._startup_done = asyncio.Event() + self._serve_task: Optional[Awaitable[Any]] = None super().__init__(config=uvicorn.Config(app, host=host, port=port)) async def startup(self, sockets: Optional[List] = None) -> None: @@ -51,35 +52,36 @@ async def startup(self, sockets: Optional[List] = None) -> None: self.config.setup_event_loop() self._startup_done.set() - async def up(self) -> None: + async def start_up(self) -> None: """Start up server asynchronously""" self._serve_task = asyncio.create_task(self.serve()) await self._startup_done.wait() - async def down(self) -> None: + async def tear_down(self) -> None: """Shut down server asynchronously""" self.should_exit = True - await self._serve_task + if self._serve_task: + await self._serve_task @pytest.fixture -async def startup_and_shutdown_server(): +async def startup_and_shutdown_server(): # pylint: disable=unused-variable """Start server as test fixture and tear down after test""" server = UvicornTestServer() - await server.up() + await server.start_up() yield - await server.down() + await server.tear_down() @pytest.mark.asyncio -async def test_chat_simple(startup_and_shutdown_server): +async def test_chat_simple(startup_and_shutdown_server): # pylint: disable=unused-argument,redefined-outer-name """A simple websocket test""" sio = socketio.AsyncClient() future = asyncio.get_running_loop().create_future() @sio.on('chat message') - def on_message_received(data): + def on_message_received(data): # pylint: disable=unused-variable print(f"Client received: {data}") # set the result future.set_result(data) @@ -98,7 +100,7 @@ def test_chat_page(): """Check if chat page returns contents""" response = client.get("/chat") assert response.ok - fn = os.path.join(os.path.dirname(__file__), '..', 'chat.html') - print(f"Chat page: {fn}") - with open(fn, 'rb') as page: + filename = os.path.join(os.path.dirname(__file__), '..', 'chat.html') + print(f"Chat page: {filename}") + with open(filename, 'rb') as page: assert response.content == page.read() diff --git a/src/app/tests/test_main.py b/src/app/tests/test_main.py index b7bf4f6..cbb313c 100644 --- a/src/app/tests/test_main.py +++ b/src/app/tests/test_main.py @@ -1,3 +1,5 @@ +"""Main app tests""" + from fastapi.testclient import TestClient from ..main import app