{ "info": { "author": "Bjorn Madsen", "author_email": "bjorn.madsen@operationsresearchgroup.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "[![Build Status](https://travis-ci.org/root-11/maslite.svg?branch=master)](https://travis-ci.org/root-/maslite.svg?branch=master)\n\n\n# MASlite\nA multi-agent platform contrived by [Bjorn Madsen](https://uk.linkedin.com/in/bmadsen)\n\nAll right reserved © 2016-2019. All code has been written by the author in \nisolation and any similarity to other systems is purely coincidental.\n\n--------------\n\n#### MASlite explained in 60 seconds:\n\nMASlite is a simle python module for creating multi-agent simulations.\n\n- _Simple_ API: Only 3 modules to learn: Scheduler, Agent & Agent message\n- _Fast_: Handles up to 270 million messages per second\n- _Lightweight_: 52kB.\n\nIt only has 3 components:\n\n- The scheduler (main loop)\n - handles pause and proceed with a single call.\n - assures repeatability in execution, which makes agents easy to debug.\n - handles up to 270 million messages per second.\n\n- Agent's \n\n - are python classes that have setup(), update() and teardown() methods that can be customized. \n - can exchange messages using send() and receive().\n - can subscribe/unsubscribe to message classes.\n - have clocks and can set alarms.\n - can be tested individually.\n - can have independent I/O/Database interaction.\n \n- Messages\n - that have sender and receiver enable direct communication\n - that have topics and no receiver are treated as broadcasts, and sent to subscribers.\n \nThe are plenty of use-cases for MASlite:\n\n- Prototyping MASSIVE™ type games.\n- Creating data processing pipeline\n- Optimisation Engine, for:\n - Scheduling (using Bjorn Madsen's distributed scheduling method)\n - Auctions (using Dimtry Bertsekas alternating iterative auction)\n \n-------------------\n\nAll the user needs to worry about are the protocols of interaction, \nwhich conveniently may be summarised as:\n\n1. Design the messages that an agent will send or receive as regular \npython objects that inherit the necessary implementation details from \na basic `AgentMessage`. The messages must have an unambiguous `topic`.\n2. Write the functions that are supposed to execute once an agent \n receives one of the messages.\n3. Update the agents operations (`self.operations`) with a dictionary\nthat describes the relationship between `topic` and `function`.\n4. Write the update function that maintains the inner state of the agent\nusing `send` to send messages, and using `receive` to get messages.\n\nThe user can thereby create an agent using just:\n\n class HelloMessage(AgentMessage):\n def __init__(self, sender, receiver)\n super().__init__(sender=sender, receiver=receiver)\n \n \n class myAgent(Agent):\n def __init__(self):\n super().__init__()\n self.operations.update({HelloMessage.__name__: self.hello})\n \n def update(self):\n while self.messages:\n msg = self.receive()\n operation = self.operations.get(msg.topic))\n if operation is not None:\n operation(msg)\n else:\n self.logger.debug(\"%s: don't know what to do with: %s\" % (self.uuid), str(msg)))\n \n def hello(self, msg)\n print(msg)\n\n\nThat simple!\n\nThe dictionary `self.operations` which is inherited from the `Agent`-class\nis updated with `{HelloMessage.__name__: self.hello}`. `self.operations` thereby acts \nas a pointer for when a `HelloMessage` arrives, so when the agents \nupdate function is called, it will get the topic from the message's and \npoint to the function `self.hello`, where `self.hello` in this simple\nexample just prints the content of the message. \n\nMore nuanced behaviour, can also be embedded without the user having\nto worry about any externals. For example if some messages take \nprecedence over others (priority messages), the inbox should be emptied \nin the beginning of the update function for sorting. \n\nHere is an example where some topics are treated with priority over \nothers:\n\n class AgentWithPriorityInbox(Agent):\n def __init__(self):\n super().__init__()\n self.operations.update({\"1\": self.some_priority_function, \n \"2\": self.some_function, \n \"3\": self.some_function, # Same function for 2 topics.! \n \"hello\": self.hello, })\n self.priority_topics = [\"1\",\"2\",\"3\"]\n self.priority_messages = deque() # from collections import deque\n self.normal_messages = deque() # deques append and popleft are threadsafe.\n \n def update(self):\n # 1. Empty the inbox and sort the messages using the topic:\n while self.messages:\n msg = self.receive()\n if msg.topic in self.priority_topics:\n self.priority_messages.append(msg)\n else:\n self.normal_messages.append(msg)\n \n # 2. We've now sorted the incoming messages and can now extend\n # the priority message deque with the normal messages:\n self.priority_messages.extend(normal_messages)\n \n # 3. Next we process them as usual:\n while len(self.priority_messages) > 0:\n msg = self.priority_messages.popleft()\n operation = self.operations.get(msg.topic)\n if operation is not None:\n operation(msg)\n else:\n ...\n\nThe only thing which the user needs to worry about, is that the update\nfunction cannot depend on any externals. The agent is confined to\nsending (`self.send(msg)`) and receiving (`msg = self.receive()`) \nmessages which must be processed within the function `self.update`.\nAny responses to sent messages will not happen until the agent runs\nupdate again.\n\nIf any state needs to be stored within the agent, such as for example\nmemory of messages sent or received, then the agents `__init__` should\ndeclare the variables as class variables and store the information.\nCalls to databases, files, etc. can of course happen, including the usage\nof `self.setup()` and `self.teardown()` which are called when the agent\nis, respectively, started or stopped. See the boiler-plate (below) for a more \ndetailed description. \n \n### Boilerplate\n\nThe following boiler-plate allows the user to manage the whole lifecycle\nof an agent, including:\n\n1. add variables to `__init__` which can store information between updates.\n2. react to topics by extending `self.operations`\n2. extend `setup` and `teardown` for start and end of the agents lifecycle.\n4. use `update` with actions before(1), during(2) and after(3) reading messages.\n\nThere are no requirements, for using all functions. The boiler-plate merely\nseeks to illustrate typical usage.\n\nThere are also no requirements for the agent to be programmed in procedural,\nfunctional or object oriented manner. Doing that is completely up to the \nuser of MASlite.\n\n class MyAgent(Agent):\n def __init__(self):\n super().__init__()\n # add variables here.\n \n # remember to register topics and their functions:\n self.operations.update({\"topic x\": self.x,\n \"topic y\": self.y,\n \"topic ...\": self....})\n \n def setup(self):\n # add own setup operations here.\n \n # register topics with the mailman..!\n # naive:\n for topic in self.operations.keys():\n self.subscribe(topic)\n # selective\n for topic in [\"topic x\",\"topic y\",\"topic ...\"]:\n self.subscribe(topic)\n \n def teardown(self):\n # add own teardown operations here.\n \n def update(self):\n # do something before reading messages\n self.action_before_processing_messages()\n \n # read the messages\n while self.messages:\n msg = self.receive()\n \n # react immediately to some messages:\n operation = self.operations.get(msg.topic)\n if operation is not None:\n operation(msg)\n \n # react after reading all messages:\n self.action_after_processing_all_messages()\n \n # Functions added by the user that are not inherited from the \n # `Agent`-class. If the `update` function should react on these,\n # the topic of the message must be in the self.operations dict.\n \n def action_before_processing_messages(self)\n # do something.\n \n def action_after_processing_all_messages(self)\n # do something. Perhaps send a message to somebody that update is done?\n msg = DoneMessages(sender=self, receiver=SomeOtherAgent)\n self.send(msg)\n \n def x(msg):\n # read msg and send a response\n from_ = msg.sender\n response = SomeMessage(sender=self, receiver=from_) \n self.send(response)\n \n def y(msg):\n # ...\n \n\n### Messages\n\nMessages are objects and are required to use the base class `AgentMessage`.\n\nWhen agents receive messages they should be interpreted by their topic, which\nshould (by convention) also be the class name of the message. Practice has shown\nthat there are no obvious reasons where this convention shouldn't apply, so \nmessages which don't have a topic declared explicitly inherit the class name. \nAn example is shown below:\n\n >>> from maslite import AgentMessage\n >>> class MyMsg(AgentMessage):\n ... def __init__(sender, receiver):\n ... super().__init__(sender=sender, receiver=receiver)\n ...\n \n >>> m = MyMsg(sender=1, receiver=2)\n >>> m.topic\n \n 'MyMsg'\n\nAdding functions to messages. Below is an example of a message with it's own\nfunction(s): \n\n class DatabaseUpdateMessage(AgentMessage):\n \"\"\" Description of the message \"\"\"\n def __init__(self, sender, senders_db_alias):\n super().__init__(sender=sender, receiver=DatabaseAgent.__name__)\n self.senders_db_alias\n self._states = {1: 'new', 2: 'read'} \n self._state = 1\n \n def get_senders_alias(self):\n return self.senders_db_alias\n \n def __next__(self)\n if self._state + 1 <= max(self._states.keys()):\n self._state += 1\n \n def state(self):\n return self._states[self._state]\n\nThe class `DatabaseUpdateMessage` is subclassed from the `AgentMessage` so that the basic message\nhandling properties are available for the DatabaseUpdateMessage. This helps the user as s/he doesn't\nneed to know anything about how the message handling system works.\n\nThe init function requires a sender, which normally defaults to the agent's `self`.\nThe `AgentMessage` knows that if it gets an agent in it's `__init__` call, it will\nobtain the agents UUID and use that. Similar applies to a receiver, where the typical\noperation is based on that the local agent gets a message from the sender and only \nknows the sender based on msg.get_sender() which returns the sending agents UUID. \nIf the sender might change UUID, in the course of multiple runs, the local agent \nshould be instructed to use, for example, the `senders_db_alias`. For the purpose\nof illustration, the message above contains the function `get_senders_alias` which\nthen can be persistent over multiple runs.\n\nThe message is also designed to be returned to save pythons garbage collector:\nWhen the DatabaseAgent receives the message, the `__next__`-function allows the\nagent to call `next(msg)` to progress it's `self._state` from '1' (new) to '2' (read)\nbefore returning it to the sender using 'self.send(msg)'. In such case it is \nimportant that the DatabaseAgent doesn't store the message in its variables, as\nthe message must __not__ have any open object pointers when sent. This is due to\nmultiprocessing which uses `multiprocessing.queue`s for exchanging messages, which\nrequire that `Agent`s and `AgentMessage`s can be pickled.\n\nIf an `Agent` can't be pickled when added to the `Scheduler`, the scheduler will\nraise an error explaining that the are open pointer references. Messages are a \nbit more tolerant as the `mailman` that manages the messages will try to send\nthe message and hope that the shared pointer will not cause conflicts. If sharing\nof object pointers is required by the user (for example during prototyping) the \nscheduler must be set up with `number_of_multiprocessors=0` which forces the \nscheduler to run single-process-single-threaded. \n\n\n__Message Conventions__:\n\n* Messages which have `None` as receiver are considered broadcasts. The logic is \nthat if you don't know who exactly you are sending it to, send it it to `None`, and\nyou might get a response if any other agent react on the topic of the message.\nThe magic behind the scenes is handled by the schedulers mailmanager (`mailman`) \nwhich keeps track of all topics that any `Agent` subscribes to. \nBy convention the topic of the message should be `self.__class__.__name__`.\n\n* Messages which have a `class.__name__` as receiver, will be received by all agents\nof that class.\n\n* Messages which have a particular UUID as receiver, will be received by the agent \nholding that UUID. If anyone other agent is tracking that UUID, by subscribing to\nit, then the tracking agent will receive a `deepcopy` of the message, and not the \noriginal. \n\n* To get the UUID of the sender the method `msg.sender` is available.\n\n* To subscribe/unsubscribe to messages the agents should use the `subscribe`\nfunction directly.\n\nThese methods are run when the agent is added (`setup`) to, or removed from (`teardown`), the \nscheduler. The internal operation of the agents `run` method guarantees this:\n\n def run(self):\n \"\"\" The main operation of the Agent. \"\"\"\n if not self.is_setup():\n self.setup()\n if not self._quit:\n self.update()\n if self._quit:\n self.teardown()\n\nThe can extend the setup methods either by writing their own `self.setup`-method \n(_recommended approach_).\n\n\n### How to load data from a database connection \n\nWhen agents are added to the scheduler `setup` is run.\nWhen agents are removed from MASlite `teardown` is run.\n\nif agents are added and removed iteratively, they will load their \nstate during `setup` and store it during `teardown` from some database. \nIt is not necessary to let the scheduler know where the database is. \nThe agents can keep track of this themselves. \n\nThough the user might find it attractice to use `uuid` to identify,\na particular `Agent` the user should keep \nin mind that the UUID is unique with __every__ creation and destruction \nof the agent. To expect or rely on the UUID to be persistent would lead \nto logical fallacy.\n\nThe user must use`setup` and `teardown` and include a naming convention \nthat assures that the agent doesn't depend on the UUID. For example:\n\n # get the data from the previously stored agent\n begin transaction:\n id = SELECT agent_id FROM stored_agents WHERE agent_alive == False LIMIT 1;\n UPDATE stored_agents WHERE agent_id == id VALUES (agent_live = TRUE);\n properties = SELECT * FROM stored_agents WHERE agent_id == id;\n end transaction;\n # Finally let the agent load the properties:\n self.load(properties) \n\nAn approach such as above assures that the agent that is revived has no \ndependency to the UUID.\n\n### Getting started\n\nTo get started only 3 steps are required:\n\nStep 1. setup a scheduler\n\n >>> from maslite import Agent, Scheduler\n >>> s = Scheduler(number_of_multi_processors=0)\n \nStep 2. create agents which have `setup`, `teardown` and `update` methods.\n\n >>> class MyAgent(Agent):\n ... def __init__(self):\n ... super().__init__()\n ... def setup(self):\n ... pass\n ... def teardown(self):\n ... pass\n ... def update(self):\n ... pass\n \n >>> m = MyAgent()\n >>> s.add(m)\n 2017-02-11 15:05:27,171 - DEBUG - Registering agent MyAgent 331228774898081874557512062996431768652\n\nStep 3. run the scheduler (nothing happens here)\n\n >>> s.run(pause_if_idle=True)\n 2017-02-11 15:09:20,120 - DEBUG - Pausing Scheduler, use 'run()' to continue. Use 'stop()' to shutdown remote processors.\n\nOther methods such as `s.run(seconds=None, iterations=None, \npause_if_idle=False)` can be applied as the user,\nfinds it suitable.\n\nStep 4. call the schedulers `stop` method to gracefully execute the `teardown` method\non all agents as a part of the shutdown procedure.\n\n >>> s.stop()\n 2017-02-11 15:09:37,055 - DEBUG - Scheduler shutdown complete.\n\n\n### Debugging with pdb or breakpoints (PyCharm)\n\nDebugging is easily performed by putting breakpoint at the beginning of\nthe update function. In that way you can watch what happens inside the \nagent during its state-update.\n\n### Typical mistakes\n\nThe user constructs the agent correctly with:\n \n1. the methods `update`, `send`, `receive`, `setup` and `teardown`,\n2. adding the agent to the scheduler using `scheduler.add(agent)`.\n3. runs the scheduler using `scheduler.run()`, \n\n...but... \n\nQ: The agents don't seem to update?\n\nA: The agents are not getting any messages and are therefore not updated.\nThis is correct behaviour, as `update` only should run when there are\nnew messages! \nTo force agents to run `update` in every scheduling cycle, use the hidden \nmethod: `agent.keep_awake=True`. Doing this blindly however is a poor design\nchoice if the agent merely is polling for data. For this purpose \n`agent.set_alarm_clock(agent.now()+1)` should be used, as this allows the\nagent to sleep for 1 second and the be \"woken up\" by the alarm message.\n\nThe reason it is recommended to use the alarm instead of setting \n`keep_awake=True` is that the workload of the system remains transparent \nat the level of message exchange. Remember that the internal state of \nthe agents should always be hidden whilst the messages should be \nindicative of any activity. \n\n### Adjust runspeed using the clock.\n\nThe clock is a powertool in MASlite that should be studied. \nThe clock has the ability to:\n\n* run at speeds `-inf`; `+inf` and any floating point progressing in between. \n\n* start at time `-inf`; `+inf` and any floating point time in between.\n\nThe clock is set using the api calls to the clock:\n\n >>> s = Scheduler()\n 2017-02-11 15:15:00,197 - INFO - Scheduler is running with uuid: 206586991924651126011034509456004484857\n 2017-02-11 15:15:00,197 - DEBUG - Registering agent Clock 237028863335333747268219642853960174161\n 2017-02-11 15:15:00,197 - DEBUG - Registering agent MailMan 108593288939288121173991719827939198422\n >>> s.now()\n 0\n >>> s.clock.time = 1000\n >>> s.now()\n 1000\n >>> s.run(seconds=5)\n 2017-02-11 15:16:49,395 - DEBUG - Pausing Scheduler, use 'run()' to continue. Use 'stop()' to shutdown remote processors.\n >>> s.now()\n 1005\n >>> s.clock.clock_speed = 200\n >>> s.now()\n 1005\n >>> s.run(seconds=5)\n 2017-02-11 15:17:41,355 - DEBUG - Pausing Scheduler, use 'run()' to continue. Use 'stop()' to shutdown remote processors.\n >>> s.now()\n 2005\n >>> s.now()\n 2005\n \n\nIn the calls (above) the scheduler first sets the time to `1000` (whatever that is).\nusing `s.clock.time`. Next it sets the clock speed using `s.clock.clock_speed`\nto 200 times real-time\n\n\n1. `clock.time = `__time in seconds__. Typically time is set to time since 1970-01-01T00:00:00.000000 - the UNIX epoch - using `time.time()`\n2. `clock.clock_speed = 1.000000`\n\nIf the clock is set to run with `clock.clock_speed = None`, the scheduler\nwill ask the clock to progress in jumps, so which behaves like follows: (pseudo code):\n\n if Clock.clock_speed is None:\n if not mailman.messages:\n if self.pending_tasks() == 0:\n Ask clock to set time to the time of the next event.\n else: wait for multiprocessor to return computed agent.\n else: continue until no new messages\n else: continue clock as normal.\n\nTo adjust the timing during a simulation (for whatever reason), the \nscheduler should be primed with messages:\n\n1. run at maximum speed: `self.set_new_clock_speed_as_timed_event(start_time=now(), speed=None)`\n2. set clock to 10x speed 1 hour into the simulation: `set_runtime(start_time=now()+1*60*60, speed=10)` This will take 6 minutes in real-time.\n3. set the clock to 1x (real-time) speed 3 hours into the simulation: `set_runtime(start_time=now()+3*60*60, speed=1)` This will take 1 hour in real time.\n4. set clock to 10x speed 4 hour into the simulation: `set_runtime(start_time=now()+4*60*60, speed=10)`\n5. set the clock to 'None' to run as fast as possible for the rest of the simulation: `set_runtime(start_time=now()+6*60*60, speed=None)`\n\nNote: The clock_speed can be set as an argument in the schedulers `run` function:\n\n Scheduler.run(seconds=None, iterations=None, pause_if_idle=False,\n clock_speed=1.0)\n\n...", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/root-11/maslite", "keywords": "multi agent system MAS", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "MASlite", "package_url": "https://pypi.org/project/MASlite/", "platform": "any", "project_url": "https://pypi.org/project/MASlite/", "project_urls": { "Homepage": "https://github.com/root-11/maslite" }, "release_url": "https://pypi.org/project/MASlite/2019.7.17.55529/", "requires_dist": null, "requires_python": "", "summary": "A lightweight multi-agent system", "version": "2019.7.17.55529" }, "last_serial": 5546178, "releases": { "2019.5.20.38037": [ { "comment_text": "", "digests": { "md5": "1ba45da526de39a6127705a75291639d", "sha256": "aaf5acf02d90d3ed9b76f959a9876111de8e68fb4da61edbf6c2167d986a938a" }, "downloads": -1, "filename": "MASlite-2019.5.20.38037.tar.gz", "has_sig": false, "md5_digest": "1ba45da526de39a6127705a75291639d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22820, "upload_time": "2019-05-20T09:34:39", "url": "https://files.pythonhosted.org/packages/0b/a3/70fdd0b414a6da19900d3cb060efbab0ce5115832b367890e0a09f17a036/MASlite-2019.5.20.38037.tar.gz" } ], "2019.5.20.41895": [ { "comment_text": "", "digests": { "md5": "f3255608b9b3a6e7fdade5d2cc8d599a", "sha256": "a61e33e00c2138972e231e5733e3f746b6e722dc1edf45f122280ce2f2451c9b" }, "downloads": -1, "filename": "MASlite-2019.5.20.41895.tar.gz", "has_sig": false, "md5_digest": "f3255608b9b3a6e7fdade5d2cc8d599a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22841, "upload_time": "2019-05-20T10:39:35", "url": "https://files.pythonhosted.org/packages/57/d7/93f6b3707eebae742e4d77a2d7c1d59f915cb4eb2a4fc5fbe55246ba176e/MASlite-2019.5.20.41895.tar.gz" } ], "2019.5.20.48214": [ { "comment_text": "", "digests": { "md5": "6cc67f961decb633386e3dec9bbcaf38", "sha256": "31dfbdcc527edad426d6634424d71b0d83e6202c1c99799d6ada8cda333d97c1" }, "downloads": -1, "filename": "MASlite-2019.5.20.48214.tar.gz", "has_sig": false, "md5_digest": "6cc67f961decb633386e3dec9bbcaf38", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22862, "upload_time": "2019-05-20T12:29:05", "url": "https://files.pythonhosted.org/packages/ab/d7/8569d2e3e561ceeade164fa15a451677085b36901eddf0ee9e7f16bf69eb/MASlite-2019.5.20.48214.tar.gz" } ], "2019.5.9.45023": [ { "comment_text": "", "digests": { "md5": "72b2d7dd05cacb955cce47270a5d4edc", "sha256": "b95db2e546a9dbb0b6f337221afd1c06c19a0fc3d03b3a0fefaef8c6d0c4e790" }, "downloads": -1, "filename": "MASlite-2019.5.9.45023.tar.gz", "has_sig": false, "md5_digest": "72b2d7dd05cacb955cce47270a5d4edc", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12704, "upload_time": "2019-05-09T11:33:55", "url": "https://files.pythonhosted.org/packages/6e/72/3e1fcf2e9d735d0a1574b1920de2cf13ccb6428e3d389bcd44869a25b170/MASlite-2019.5.9.45023.tar.gz" } ], "2019.5.9.49364": [ { "comment_text": "", "digests": { "md5": "b27ca4ead5d8da37ace1b4217181d69d", "sha256": "7559dacbba929b6fb4af67159675d5200744d87c5b4c3b5eaf5f765fba86079c" }, "downloads": -1, "filename": "MASlite-2019.5.9.49364.tar.gz", "has_sig": false, "md5_digest": "b27ca4ead5d8da37ace1b4217181d69d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20360, "upload_time": "2019-05-09T12:44:50", "url": "https://files.pythonhosted.org/packages/c0/8b/7c0ec7b6c60ead91969715e9932cdfb7c654efbf3b43e00b90184755f03f/MASlite-2019.5.9.49364.tar.gz" } ], "2019.5.9.50822": [ { "comment_text": "", "digests": { "md5": "8f20235d17ec5b899f90ba0096d19c85", "sha256": "9a01365b227432214ed555a89236415986c65c10b8a99f4a087e6ec54938017e" }, "downloads": -1, "filename": "MASlite-2019.5.9.50822.tar.gz", "has_sig": false, "md5_digest": "8f20235d17ec5b899f90ba0096d19c85", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20382, "upload_time": "2019-05-09T13:07:51", "url": "https://files.pythonhosted.org/packages/36/7d/5a4c7d916ce1f7b38dd78c34075092d4b01e02115f77f7d9e49a9e1c84a5/MASlite-2019.5.9.50822.tar.gz" } ], "2019.5.9.51003": [ { "comment_text": "", "digests": { "md5": "9f0e0c9cf5cf6fcd68192e8dd7e3fcab", "sha256": "015b6444c9b8b4dcf80abb0906250f7cac9126634cac36a4b60556088dd10e2a" }, "downloads": -1, "filename": "MASlite-2019.5.9.51003.tar.gz", "has_sig": false, "md5_digest": "9f0e0c9cf5cf6fcd68192e8dd7e3fcab", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 20381, "upload_time": "2019-05-09T13:10:59", "url": "https://files.pythonhosted.org/packages/27/27/a349af0b1bb414919a4b20deb976707822a259e5d81f62a38c158e33b67c/MASlite-2019.5.9.51003.tar.gz" } ], "2019.5.9.52401": [ { "comment_text": "", "digests": { "md5": "b43ac5d0c7739c9f4e9f6a237a01f518", "sha256": "dc256ef999e17510f36180ff0e00ae2a669a4579e59b483a73ccbb7b45ad346b" }, "downloads": -1, "filename": "MASlite-2019.5.9.52401.tar.gz", "has_sig": false, "md5_digest": "b43ac5d0c7739c9f4e9f6a237a01f518", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22743, "upload_time": "2019-05-09T13:34:08", "url": "https://files.pythonhosted.org/packages/f5/b3/8b0d71a09629afdd0e35da7eb39a316da5475056a8b3be0b27dc10c6fa13/MASlite-2019.5.9.52401.tar.gz" } ], "2019.6.14.39347": [ { "comment_text": "", "digests": { "md5": "9ecc41ab43de180ff531192d767d03cf", "sha256": "a03b6bb67858b6a49f1e22bbb96348d4cdd133bc4b8721321fe9974b6017c735" }, "downloads": -1, "filename": "MASlite-2019.6.14.39347.tar.gz", "has_sig": false, "md5_digest": "9ecc41ab43de180ff531192d767d03cf", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27959, "upload_time": "2019-06-14T09:57:21", "url": "https://files.pythonhosted.org/packages/7b/3c/ac3f14527d88c6920f2fad20181147af05d79310f8831a17c64026a11456/MASlite-2019.6.14.39347.tar.gz" } ], "2019.7.17.55529": [ { "comment_text": "", "digests": { "md5": "80f9ffe42c97aa9fcf558d7d0ceb2d7c", "sha256": "08f3c1a9f687c2da86f00f72233fa77c069f57fdea5fb4670ed9da620e851f04" }, "downloads": -1, "filename": "MASlite-2019.7.17.55529.tar.gz", "has_sig": false, "md5_digest": "80f9ffe42c97aa9fcf558d7d0ceb2d7c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22332, "upload_time": "2019-07-17T14:26:56", "url": "https://files.pythonhosted.org/packages/e2/39/9a5d145a28a91c062455acfbe2e242cd6d2b4fc7f309d920125f095a3664/MASlite-2019.7.17.55529.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "80f9ffe42c97aa9fcf558d7d0ceb2d7c", "sha256": "08f3c1a9f687c2da86f00f72233fa77c069f57fdea5fb4670ed9da620e851f04" }, "downloads": -1, "filename": "MASlite-2019.7.17.55529.tar.gz", "has_sig": false, "md5_digest": "80f9ffe42c97aa9fcf558d7d0ceb2d7c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 22332, "upload_time": "2019-07-17T14:26:56", "url": "https://files.pythonhosted.org/packages/e2/39/9a5d145a28a91c062455acfbe2e242cd6d2b4fc7f309d920125f095a3664/MASlite-2019.7.17.55529.tar.gz" } ] }