diff --git a/Pipfile b/Pipfile index 5b1e94e..48f3680 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ pytest = "*" twine = "*" [packages] +aiosocksy = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b7f7656 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,339 @@ +{ + "_meta": { + "hash": { + "sha256": "65699117e134e0d00bbe342678e8b14b774c00316bdf64a1336e182ede071624" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "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" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.6.3" + }, + "aiosocksy": { + "hashes": [ + "sha256:d08822b835c91b7d9199f9d552774d8a52766a7e2912e1f22308b5299d4b216e" + ], + "index": "pypi", + "version": "==0.1.2" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "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" + }, + "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" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", + "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.4.0" + }, + "attrs": { + "hashes": [ + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" + }, + "bleach": { + "hashes": [ + "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", + "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.2.1" + }, + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "colorama": { + "hashes": [ + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.4" + }, + "docutils": { + "hashes": [ + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.16" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" + }, + "keyring": { + "hashes": [ + "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", + "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" + ], + "markers": "python_version >= '3.6'", + "version": "==21.4.0" + }, + "packaging": { + "hashes": [ + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4" + }, + "pkginfo": { + "hashes": [ + "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", + "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + ], + "version": "==1.5.0.1" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" + }, + "pygments": { + "hashes": [ + "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998", + "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7" + ], + "markers": "python_version >= '3.5'", + "version": "==2.7.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9", + "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92" + ], + "index": "pypi", + "version": "==6.1.1" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" + }, + "readme-renderer": { + "hashes": [ + "sha256:3176d93d2c21960fb7f7458073b9f1e5dd14a5af0ec6af4afb957337dbb6a326", + "sha256:e6871b10341cdd85ade112fb8503b31301d1ca12c0c3b3e1358855329519968b" + ], + "version": "==27.0" + }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.24.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "rfc3986": { + "hashes": [ + "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", + "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + ], + "version": "==1.4.0" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.15.0" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + }, + "tqdm": { + "hashes": [ + "sha256:43ca183da3367578ebf2f1c2e3111d51ea161ed1dc4e6345b86e27c2a93beff7", + "sha256:69dfa6714dee976e2425a9aab84b622675b7b1742873041e3db8a8e86132a4af" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1'", + "version": "==4.50.2" + }, + "twine": { + "hashes": [ + "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", + "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "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": "==1.25.10" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + } + } +} diff --git a/README.md b/README.md index e21c786..2976af0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Python AMCP Client Library -> v0.1.0 +> v0.2.0 ## Introduction @@ -24,6 +24,15 @@ client = Client() client.connect("caspar-server.local", 6969) # defaults to 127.0.0.1, 5250 ``` +Support for `asyncio` module: +```python +import asyncio +from amcp_pylib.core import ClientAsync + +client = ClientAsync() +asyncio.new_event_loop().run_until_complete(client.connect("caspar-server.local", 6969)) +``` + ### Sending commands ```python @@ -41,8 +50,8 @@ print(response) ``` ```shell -201(VERSION) ['2.0.7.e9fc25a Stable'] -0(EMPTY) ['SERVER SENT NO RESPONSE'] +2.0.7.e9fc25a Stable (201 - VERSION) +SERVER SENT NO RESPONSE (0 - EMPTY) ``` All supported protocol commands are listed and documented on CasparCG's [wiki pages](https://github.com/CasparCG/help/wiki/AMCP-Protocol#table-of-contents). diff --git a/amcp_pylib/core/__init__.py b/amcp_pylib/core/__init__.py index d632437..f1b62ef 100644 --- a/amcp_pylib/core/__init__.py +++ b/amcp_pylib/core/__init__.py @@ -1,10 +1,14 @@ from .client import Client +from .client_async import ClientAsync from .command import Command, command_syntax from .connection import Connection +from .connection_async import ConnectionAsync __all__ = [ "Client", + "ClientAsync", "Command", "command_syntax", "Connection", + "ConnectionAsync", ] diff --git a/amcp_pylib/core/client.py b/amcp_pylib/core/client.py index 7e45bc8..541bb4f 100644 --- a/amcp_pylib/core/client.py +++ b/amcp_pylib/core/client.py @@ -1,5 +1,6 @@ -from .connection import Connection from .command import Command +from .connection import Connection +from .client_base import ClientBase from amcp_pylib.response import ( Base as ResponseBase, @@ -7,10 +8,8 @@ ) -class Client: +class Client(ClientBase): """ Connection client class. """ - - connection: Connection = None def connect(self, host: str = "127.0.0.1", port: int = 5250): """ Initialize TCP connection to given host address and port. """ diff --git a/amcp_pylib/core/client_async.py b/amcp_pylib/core/client_async.py new file mode 100644 index 0000000..dc87446 --- /dev/null +++ b/amcp_pylib/core/client_async.py @@ -0,0 +1,27 @@ +from .command import Command +from .client_base import ClientBase +from .connection_async import ConnectionAsync + +from amcp_pylib.response import ( + Base as ResponseBase, + Factory as ResponseFactory, +) + + +class ClientAsync(ClientBase): + connection: ConnectionAsync + + async def connect(self, host: str = "127.0.0.1", port: int = 5250): + if not self.connection: + self.connection = ConnectionAsync(host, port) + + async def send(self, command: Command) -> ResponseBase: + return await self.send_raw(bytes(command)) + + async def send_raw(self, data: bytes) -> ResponseBase: + await self.connection.send(data) + return await self.process_response() + + async def process_response(self) -> ResponseBase: + data = await self.connection.receive() + return ResponseFactory.create_from_bytes(data) diff --git a/amcp_pylib/core/client_base.py b/amcp_pylib/core/client_base.py new file mode 100644 index 0000000..715bcb5 --- /dev/null +++ b/amcp_pylib/core/client_base.py @@ -0,0 +1,31 @@ +from abc import ABCMeta, abstractmethod +from .command import Command +from .connection_base import ConnectionBase + +from amcp_pylib.response import Base as ResponseBase + + +class ClientBase(metaclass=ABCMeta): + """ Connection client class. """ + + connection: ConnectionBase = None + + @abstractmethod + def connect(self, host: str = "127.0.0.1", port: int = 5250): + """ Initialize TCP connection to given host address and port. """ + pass + + @abstractmethod + def send(self, command: Command) -> ResponseBase: + """ Convert command to bytes and then send it via established server connection. """ + pass + + @abstractmethod + def send_raw(self, data: bytes) -> ResponseBase: + """ Send bytes via established server connection. """ + pass + + @abstractmethod + def process_response(self) -> ResponseBase: + """ Receive data from server, parse it and create corresponding class. """ + pass diff --git a/amcp_pylib/core/connection.py b/amcp_pylib/core/connection.py index 2ceef4a..34de795 100644 --- a/amcp_pylib/core/connection.py +++ b/amcp_pylib/core/connection.py @@ -1,7 +1,9 @@ import socket +from .connection_base import ConnectionBase -class Connection: + +class Connection(ConnectionBase): """ Represents TCP connection to target server. """ @@ -10,36 +12,29 @@ class Connection: s: socket.socket = None def __init__(self, host: str, port: int): - """ Initializes Connection class and creates connection to server. """ - - # Get necessary address information + # get necessary address information address_info = socket.getaddrinfo(host, port)[0] - # Create connection from information + # create connection from information self.connect(address_info[0], address_info[4]) def connect(self, address_family: int, address_target: tuple): - """ Creates connection to server. """ - - # Create required TCP socket + # create required TCP socket self.s = socket.socket(address_family, socket.SOCK_STREAM) - # Connect to provided target + # connect to provided target self.s.connect(address_target) def disconnect(self): - """ Closes active socket. """ self.s.close() def send(self, data: bytes): - """ Sends data through connection's socket stream. """ self.s.sendall(data) def receive(self) -> bytes: - """ Reads data from connection's socket stream. """ data = bytes() while True: - new_data = self.s.recv(1024) + new_data = self.s.recv(1500) data += new_data - if len(new_data) < 1024: + if len(new_data) < 1500: break return data diff --git a/amcp_pylib/core/connection_async.py b/amcp_pylib/core/connection_async.py new file mode 100644 index 0000000..1f0e2d3 --- /dev/null +++ b/amcp_pylib/core/connection_async.py @@ -0,0 +1,44 @@ +import socket +import asyncio +from asyncio import StreamReader, StreamWriter + +from .connection_base import ConnectionBase + + +class ConnectionAsync(ConnectionBase): + """ + Represents TCP connection to target server. + """ + + # TCP communication reader + reader: StreamReader = None + # TCP communication writer + writer: StreamWriter = None + + def __init__(self, host: str, port: int): + # get necessary address information + address_info = socket.getaddrinfo(host, port)[0] + # create connection from information + self.connect(address_info[0], address_info[4]) + + async def connect(self, address_family: int, address_target: tuple): + # create required TCP socket + self.reader, self.writer = await asyncio.open_connection() + # connect to provided target + await self.connect(address_family, address_target) + + async def disconnect(self): + self.writer.close() + + async def send(self, data: bytes): + self.writer.write(data) + + async def receive(self) -> bytes: + data = bytes() + while True: + new_data = await self.reader.read(1500) + data += new_data + if len(new_data) < 1500: + break + + return data diff --git a/amcp_pylib/core/connection_base.py b/amcp_pylib/core/connection_base.py new file mode 100644 index 0000000..75312cc --- /dev/null +++ b/amcp_pylib/core/connection_base.py @@ -0,0 +1,27 @@ +from abc import ABCMeta, abstractmethod + + +class ConnectionBase(metaclass=ABCMeta): + """ + Represents TCP connection to target server. + """ + + @abstractmethod + def connect(self, address_family: int, address_target: tuple): + """ Creates connection to server. """ + pass + + @abstractmethod + def disconnect(self): + """ Closes active socket. """ + pass + + @abstractmethod + def send(self, data: bytes): + """ Sends data through connection's socket stream. """ + pass + + @abstractmethod + def receive(self) -> bytes: + """ Reads data from connection's socket stream. """ + pass diff --git a/amcp_pylib/core/syntax/command_argument.py b/amcp_pylib/core/syntax/command_argument.py index 6633296..18b63a9 100644 --- a/amcp_pylib/core/syntax/command_argument.py +++ b/amcp_pylib/core/syntax/command_argument.py @@ -55,6 +55,7 @@ def check_value_type(self, value): allowed_type=self.required_datatype, ) ) + elif self.required_keywords and str(value) not in self.required_keywords: raise RuntimeError( "Value '{arg_value}' of argument '{arg_identifier}' is not valid. " diff --git a/amcp_pylib/core/syntax/command_group.py b/amcp_pylib/core/syntax/command_group.py index 93a2ba4..66c7c53 100644 --- a/amcp_pylib/core/syntax/command_group.py +++ b/amcp_pylib/core/syntax/command_group.py @@ -1,3 +1,5 @@ +import typing + from .token import Token from .token_types import TokenType from .command_argument import CommandArgument @@ -12,7 +14,7 @@ class CommandGroup: # other groups within this one subgroups: list = None # this groups arguments - arguments: list = None + arguments: typing.List[CommandArgument] = None # concrete order of elements within this group to be rendered display_order: list = None # is this group required? diff --git a/amcp_pylib/core/syntax/parser.py b/amcp_pylib/core/syntax/parser.py index d50ada2..26ad59f 100644 --- a/amcp_pylib/core/syntax/parser.py +++ b/amcp_pylib/core/syntax/parser.py @@ -20,13 +20,13 @@ def get_token(self, token_type=None) -> Token: """ Gets next token from scanner. Allows to apply token type constraint. - :param token_type: [int|list] + :param token_type: [TokenType|list] :returns: Received token instance. :raises RuntimeError: Received token's type not allowed by specified constraint. """ token_types = token_type - if isinstance(token_type, int): + if isinstance(token_type, TokenType): token_types = [token_type] token = self.scanner.get_next_token() @@ -45,7 +45,7 @@ def try_get_token(self, token_type, return_on_success=False): """ Tries to get token of specified type(s). Returns token on unsuccessful attempt (type mismatch). - :param token_type: [int|list] Requested token type(s). + :param token_type: [TokenType|list] Requested token type(s). :param return_on_success: Returns token even when token of specified type was received. :returns: True or Token instance on success (depending return_on_success). False on type mismatch. @@ -79,7 +79,7 @@ def return_token(self, token: Token): """ self.scanner.return_token(token) - def parse(self): + def parse(self) -> CommandGroup: """ Tries to parse syntax definition string. diff --git a/amcp_pylib/core/syntax/scanner.py b/amcp_pylib/core/syntax/scanner.py index 0402d68..ac2d459 100644 --- a/amcp_pylib/core/syntax/scanner.py +++ b/amcp_pylib/core/syntax/scanner.py @@ -56,29 +56,32 @@ def get_next_token(self) -> Token: # match token type if match["keyword"]: token_type = TokenType.KEYWORD + elif match["constant"]: token_type = TokenType.CONSTANT - if token_content_full is ' ': + if token_content_full == ' ': token_type = TokenType.CONSTANT_SPACE + elif match["identifier"]: if token_content in ["int", "string", "float"]: token_type = TokenType.TYPE else: token_type = TokenType.IDENTIFIER + elif match["operators"]: - if token_content is '[': + if token_content == '[': token_type = TokenType.REQUIRED_OPEN - elif token_content is ']': + elif token_content == ']': token_type = TokenType.REQUIRED_CLOSE - elif token_content is '{': + elif token_content == '{': token_type = TokenType.OPTIONAL_OPEN - elif token_content is '}': + elif token_content == '}': token_type = TokenType.OPTIONAL_CLOSE - elif token_content is '|': + elif token_content == '|': token_type = TokenType.OPERATOR_OR - elif token_content is ':': + elif token_content == ':': token_type = TokenType.OPERATOR_TYPE - elif token_content is ',': + elif token_content == ',': token_type = TokenType.OPERATOR_COMMA return Token(token_type, token_content_full) diff --git a/amcp_pylib/core/syntax/token.py b/amcp_pylib/core/syntax/token.py index a3e20ab..7d4ed6b 100644 --- a/amcp_pylib/core/syntax/token.py +++ b/amcp_pylib/core/syntax/token.py @@ -6,7 +6,7 @@ class Token: Class representing token instance. """ - token_type: int = TokenType.CONSTANT + token_type: TokenType = TokenType.CONSTANT token_content: str = None def __init__(self, token_type=TokenType.UNDEFINED, token_content=""): @@ -21,7 +21,7 @@ def __str__(self) -> str: type=TokenType.to_str(self.token_type) ) - def get_type(self) -> int: + def get_type(self) -> TokenType: """ Returns token type. """ return self.token_type diff --git a/amcp_pylib/core/syntax/token_types.py b/amcp_pylib/core/syntax/token_types.py index 3e2f22a..a3c5365 100644 --- a/amcp_pylib/core/syntax/token_types.py +++ b/amcp_pylib/core/syntax/token_types.py @@ -1,29 +1,32 @@ -class TokenType: +from enum import Enum, auto + + +class TokenType(Enum): """ Class holding existing token types. """ - UNDEFINED = 0x00 - KEYWORD = 0x01 - CONSTANT = 0x02 - CONSTANT_SPACE = 0x03 - IDENTIFIER = 0x04 - TYPE = 0x05 + UNDEFINED = auto() + KEYWORD = auto() + CONSTANT = auto() + CONSTANT_SPACE = auto() + IDENTIFIER = auto() + TYPE = auto() - OPERATOR_OR = 0x0A - OPERATOR_TYPE = 0x0B - OPERATOR_COMMA = 0x0C + OPERATOR_OR = auto() + OPERATOR_TYPE = auto() + OPERATOR_COMMA = auto() - REQUIRED_OPEN = 0xA0 - REQUIRED_CLOSE = 0xA1 + REQUIRED_OPEN = auto() + REQUIRED_CLOSE = auto() - OPTIONAL_OPEN = 0xB0 - OPTIONAL_CLOSE = 0xB1 + OPTIONAL_OPEN = auto() + OPTIONAL_CLOSE = auto() - END = 0xFF + END = auto() @staticmethod - def to_str(token_type: int) -> str: + def to_str(token_type) -> str: """ Converts numeric token type to readable string. """ return { TokenType.UNDEFINED: "undefined", diff --git a/amcp_pylib/response/base.py b/amcp_pylib/response/base.py index 7d8373f..38ccb3a 100644 --- a/amcp_pylib/response/base.py +++ b/amcp_pylib/response/base.py @@ -1,3 +1,5 @@ +import typing + from .parser import Parser @@ -8,16 +10,14 @@ class Base: code: int = 0 code_description: str = None - data: str = None + data: typing.List[str] = None + data_str: str = None def __init__(self, response: str): """ Initializes response Base class. """ self.code, self.code_description, self.data = Parser.parse_response_status_header(response) + self.data_str = " ".join(self.data) def __str__(self) -> str: """ Renders object as readable string. """ - return "{code}({description}) {data}".format( - code=self.code, - description=self.code_description, - data=self.data, - ) + return f"{self.data_str} ({self.code} - {self.code_description})" diff --git a/setup.py b/setup.py index 6c91594..0dff6a2 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="amcp_pylib", url="https://github.com/dolejska-daniel/amcp-pylib", - version="0.1.2", + version="0.2.0", author="Daniel Dolejska", author_email="dolejskad@gmail.com", description="AMCP (Advanced Media Control Protocol) Client Library", @@ -17,19 +17,12 @@ license="MIT", packages=find_packages(), classifiers=[ - # Development "Development Status :: 4 - Beta", - # Audience "Intended Audience :: Developers", - # License "License :: OSI Approved :: MIT License", - # Language "Natural Language :: English", - # Topics "Topic :: Multimedia :: Video", - # OS "Operating System :: OS Independent", - # Programming language "Programming Language :: Python :: 3", ], )