mirror of
https://github.com/PiBrewing/craftbeerpi4.git
synced 2024-11-25 16:38:36 +01:00
adding description
This commit is contained in:
parent
9938ce6de1
commit
3a39719957
17 changed files with 120 additions and 278 deletions
|
@ -1 +1 @@
|
||||||
__version__ = "4.0.0.15"
|
__version__ = "4.0.0.16"
|
|
@ -9,7 +9,6 @@ __all__ = ["CBPiActor",
|
||||||
"parameters",
|
"parameters",
|
||||||
"background_task",
|
"background_task",
|
||||||
"CBPiKettleLogic",
|
"CBPiKettleLogic",
|
||||||
"CBPiSimpleStep",
|
|
||||||
"CBPiException",
|
"CBPiException",
|
||||||
"KettleException",
|
"KettleException",
|
||||||
"SensorException",
|
"SensorException",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
from abc import ABCMeta
|
from abc import ABCMeta
|
||||||
import asyncio
|
import asyncio
|
||||||
from cbpi.api.extension import CBPiExtension
|
|
||||||
from cbpi.api.config import ConfigType
|
from cbpi.api.config import ConfigType
|
||||||
|
|
||||||
__all__ = ["CBPiActor"]
|
__all__ = ["CBPiActor"]
|
||||||
|
|
173
cbpi/api/step.py
173
cbpi/api/step.py
|
@ -4,11 +4,12 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod, ABCMeta
|
from abc import abstractmethod, ABCMeta
|
||||||
import logging
|
import logging
|
||||||
|
from cbpi.api.config import ConfigType
|
||||||
|
|
||||||
class CBPiStep(metaclass=ABCMeta):
|
class CBPiStep(metaclass=ABCMeta):
|
||||||
def __init__(self, cbpi, id, name, props) :
|
def __init__(self, cbpi, id, name, props) :
|
||||||
self.cbpi = cbpi
|
self.cbpi = cbpi
|
||||||
self.props = {"wohoo": 0, "count": 5, **props}
|
self.props = {**props}
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.status = 0
|
self.status = 0
|
||||||
|
@ -45,7 +46,7 @@ class CBPiStep(metaclass=ABCMeta):
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
await self.execute()
|
await self.execute()
|
||||||
except:
|
except Exception as e:
|
||||||
self._exception_count += 1
|
self._exception_count += 1
|
||||||
logging.error("Step has thrown exception")
|
logging.error("Step has thrown exception")
|
||||||
if self._exception_count >= self._max_exceptions:
|
if self._exception_count >= self._max_exceptions:
|
||||||
|
@ -55,158 +56,52 @@ class CBPiStep(metaclass=ABCMeta):
|
||||||
|
|
||||||
return (self.id, self.stop_reason)
|
return (self.id, self.stop_reason)
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def execute(self):
|
async def execute(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class CBPiSimpleStep(metaclass=ABCMeta):
|
def get_static_config_value(self,name,default):
|
||||||
|
return self.cbpi.static_config.get(name, default)
|
||||||
|
|
||||||
managed_fields = []
|
def get_config_value(self,name,default):
|
||||||
|
return self.cbpi.config.get(name, default=default)
|
||||||
|
|
||||||
def __init__(self, cbpi="", managed_fields=[], id="", name="", *args, **kwargs):
|
async def set_config_value(self,name,value):
|
||||||
self.logger = logging.getLogger(__name__)
|
return await self.cbpi.config.set(name,value)
|
||||||
self._exception_count = 0
|
|
||||||
self._interval = 0.1
|
|
||||||
self._max_exceptions = 2
|
|
||||||
self.__dirty = False
|
|
||||||
self.cbpi = cbpi
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
if managed_fields:
|
async def add_config_value(self, name, value, type: ConfigType, description, options=None):
|
||||||
self.managed_fields = managed_fields
|
await self.cbpi.add(name, value, type, description, options=None)
|
||||||
for a in managed_fields:
|
|
||||||
super(CBPiSimpleStep, self).__setattr__(a, kwargs.get(a, None))
|
|
||||||
|
|
||||||
self.is_stopped = False
|
def get_kettle(self,id):
|
||||||
self.is_next = False
|
return self.cbpi.kettle.find_by_id(id)
|
||||||
self.start = time.time()
|
|
||||||
|
|
||||||
self.logger.info(self.__repr__())
|
async def set_target_temp(self,id, temp):
|
||||||
|
await self.cbpi.kettle.set_target_temp(id, temp)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def get_sensor(self,id):
|
||||||
mf = {}
|
return self.cbpi.sensor.find_by_id(id)
|
||||||
has_cbpi = True if self.cbpi is not None else False
|
|
||||||
for f in self.managed_fields:
|
|
||||||
mf[f] = super(CBPiSimpleStep, self).__getattribute__(f)
|
|
||||||
return json.dumps(dict(type=self.__class__.__name__, id=self.id, name=self.name, has_link_to_cbpi=has_cbpi, managed_fields=mf))
|
|
||||||
|
|
||||||
def get_status(self):
|
def get_actor(self,id):
|
||||||
pass
|
return self.cbpi.actor.find_by_id(id)
|
||||||
|
|
||||||
def running(self):
|
def get_actor_state(self,id):
|
||||||
'''
|
try:
|
||||||
Method checks if the step should continue running.
|
actor = self.cbpi.actor.find_by_id(id)
|
||||||
The method will return False if the step is requested to stop or the next step should start
|
return actor.get("instance").get_state()
|
||||||
|
except:
|
||||||
|
logging.error("Faild to read actor state in step - actor {}".format(id))
|
||||||
|
return None
|
||||||
|
|
||||||
:return: True if the step is running. Otherwise False.
|
async def actor_on(self,id):
|
||||||
'''
|
|
||||||
if self.is_next is True:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.is_stopped is True:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
|
|
||||||
#while self.running():
|
|
||||||
# print(".... Step %s ...." % self.id)
|
|
||||||
# await asyncio.sleep(0.1)
|
|
||||||
'''
|
|
||||||
This method in running in the background. It invokes the run_cycle method in the configured interval
|
|
||||||
It checks if a managed variable was modified in the last exection cycle. If yes, the method will persisit the new value of the
|
|
||||||
managed property
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
'''
|
|
||||||
|
|
||||||
while self.running():
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.run_cycle()
|
await self.cbpi.actor.on(id)
|
||||||
except Exception as e:
|
except:
|
||||||
logging.exception("CBPiSimpleStep Error")
|
|
||||||
self._exception_count = self._exception_count + 1
|
|
||||||
if self._exception_count == self._max_exceptions:
|
|
||||||
self.logger.error("Step Exception limit exceeded. Stopping Step")
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
await asyncio.sleep(self._interval)
|
|
||||||
|
|
||||||
if self.is_dirty():
|
|
||||||
# Now we have to store the managed props
|
|
||||||
state = {}
|
|
||||||
for field in self.managed_fields:
|
|
||||||
|
|
||||||
state[field] = self.__getattribute__(field)
|
|
||||||
|
|
||||||
await self.cbpi.step.model.update_step_state(self.id, state)
|
|
||||||
await self.cbpi.bus.fire("step/update")
|
|
||||||
self.reset_dirty()
|
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def run_cycle(self):
|
|
||||||
'''
|
|
||||||
This method is executed in the defined interval.
|
|
||||||
That the place to put your step logic.
|
|
||||||
The method need to be overwritten in the Ccstom step implementaion
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
'''
|
|
||||||
|
|
||||||
print("NOTING IMPLEMENTED")
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def next(self):
|
async def actor_off(self,id):
|
||||||
|
try:
|
||||||
'''
|
await self.cbpi.actor.off(id)
|
||||||
Request to stop the the step
|
except:
|
||||||
|
|
||||||
:return: None
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.is_next = True
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
'''
|
|
||||||
Request to stop the step
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
'''
|
|
||||||
self.is_stopped = True
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
'''
|
|
||||||
Reset the step. This method needs to be overwritten by the custom step implementation
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
'''
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_dirty(self):
|
|
||||||
|
|
||||||
'''
|
|
||||||
Check if a managed variable has a new value
|
|
||||||
|
|
||||||
:return: True if at least one managed variable has a new value assigend. Otherwise False
|
|
||||||
'''
|
|
||||||
return self.__dirty
|
|
||||||
|
|
||||||
def reset_dirty(self):
|
|
||||||
'''
|
|
||||||
Reset the dirty flag
|
|
||||||
|
|
||||||
:return:
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.__dirty = False
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
if name != "_Step__dirty" and name in self.managed_fields:
|
|
||||||
self.__dirty = True
|
|
||||||
super(CBPiSimpleStep, self).__setattr__(name, value)
|
|
||||||
else:
|
|
||||||
super(CBPiSimpleStep, self).__setattr__(name, value)
|
|
||||||
|
|
|
@ -10,5 +10,5 @@ username: cbpi
|
||||||
password: 123
|
password: 123
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- cbpi4-ui
|
- cbpi4ui
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,8 @@
|
||||||
{
|
{
|
||||||
"basic": {
|
"basic": {
|
||||||
"name": "WOOHOo"
|
"name": ""
|
||||||
},
|
},
|
||||||
"profile": [
|
"profile": [
|
||||||
{
|
|
||||||
"id": "MSXuATqL56EAeCXrg3XLuY",
|
|
||||||
"name": "Test",
|
|
||||||
"props": {
|
|
||||||
"Param1": 123,
|
|
||||||
"Param2": "HALLO",
|
|
||||||
"Param3": 1,
|
|
||||||
"count": 6,
|
|
||||||
"wohoo": 0
|
|
||||||
},
|
|
||||||
"status": "D",
|
|
||||||
"type": "CustomStep2"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -90,7 +90,6 @@ class BasicController:
|
||||||
|
|
||||||
type = item["type"]
|
type = item["type"]
|
||||||
clazz = self.types[type]["class"]
|
clazz = self.types[type]["class"]
|
||||||
print("#####",item)
|
|
||||||
item["instance"] = clazz(self.cbpi, item["id"], item["props"])
|
item["instance"] = clazz(self.cbpi, item["id"], item["props"])
|
||||||
await item["instance"].start()
|
await item["instance"].start()
|
||||||
item["instance"].task = self._loop.create_task(item["instance"].run())
|
item["instance"].task = self._loop.create_task(item["instance"].run())
|
||||||
|
|
|
@ -227,7 +227,7 @@ class StepController:
|
||||||
async def start_step(self,step):
|
async def start_step(self,step):
|
||||||
logging.info("Start Step")
|
logging.info("Start Step")
|
||||||
step.get("instance").start()
|
step.get("instance").start()
|
||||||
step["instance"].task = self._loop.create_task(step["instance"].run(), name=step["name"])
|
step["instance"].task = self._loop.create_task(step["instance"].run())
|
||||||
step["instance"].task .add_done_callback(self.done)
|
step["instance"].task .add_done_callback(self.done)
|
||||||
step["status"] = "A"
|
step["status"] = "A"
|
||||||
|
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
|
|
||||||
from cbpi.api import *
|
|
||||||
|
|
||||||
|
|
||||||
@parameters([Property.Number(label="Param1", configurable=True),
|
|
||||||
Property.Text(label="Param2", configurable=True, default_value="HALLO"),
|
|
||||||
Property.Select(label="Param3", options=[1,2,4]),
|
|
||||||
Property.Sensor(label="Param4"),
|
|
||||||
Property.Actor(label="Param5")])
|
|
||||||
class Step2(CBPiStep):
|
|
||||||
|
|
||||||
@action(key="name2", parameters=[])
|
|
||||||
async def action2(self, **kwargs):
|
|
||||||
print("CALL ACTION")
|
|
||||||
|
|
||||||
@action(key="name", parameters=[Property.Number(label="Test", configurable=True)])
|
|
||||||
async def action(self, **kwargs):
|
|
||||||
print("CALL ACTION")
|
|
||||||
|
|
||||||
async def execute(self):
|
|
||||||
count = self.props.get("count", 0)
|
|
||||||
self.state_msg = "COUNT %s" % count
|
|
||||||
|
|
||||||
self.props["count"] += 1
|
|
||||||
await self.update(self.props)
|
|
||||||
|
|
||||||
if count >= 5:
|
|
||||||
self.next()
|
|
||||||
|
|
||||||
async def reset(self):
|
|
||||||
self.props["count"] = 0
|
|
||||||
|
|
||||||
def setup(cbpi):
|
|
||||||
'''
|
|
||||||
This method is called by the server during startup
|
|
||||||
Here you need to register your plugins at the server
|
|
||||||
|
|
||||||
:param cbpi: the cbpi core
|
|
||||||
:return:
|
|
||||||
'''
|
|
||||||
cbpi.plugin.register("CustomStep2", Step2)
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
name: DummyStep
|
|
||||||
version: 4
|
|
||||||
active: true
|
|
|
@ -20,17 +20,17 @@ except Exception:
|
||||||
import RPi.GPIO as GPIO
|
import RPi.GPIO as GPIO
|
||||||
|
|
||||||
|
|
||||||
@parameters([Property.Select(label="GPIO", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]), Property.Select(label="Inverted", options=["Yes", "No"])])
|
@parameters([Property.Select(label="GPIO", description="The GPIO Pin", options=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]), Property.Select(label="Inverted", description="No: Active on high; Yes: Active on low", options=["Yes", "No"])])
|
||||||
class GPIOActor(CBPiActor):
|
class GPIOActor(CBPiActor):
|
||||||
|
|
||||||
|
|
||||||
def get_GPIO_state(self, state):
|
def get_GPIO_state(self, state):
|
||||||
# ON
|
# ON
|
||||||
if state == 1:
|
if state == 1:
|
||||||
return 0 if self.inverted == False else 1
|
return 1 if self.inverted == False else 0
|
||||||
# OFF
|
# OFF
|
||||||
if state == 0:
|
if state == 0:
|
||||||
return 1 if self.inverted == False else 0
|
return 0 if self.inverted == False else 1
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
await super().start()
|
await super().start()
|
||||||
|
|
38
cbpi/extension/mashstep/__init__.py
Normal file
38
cbpi/extension/mashstep/__init__.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
from cbpi.api import *
|
||||||
|
|
||||||
|
|
||||||
|
@parameters([Property.Number(label="Timer", configurable=True),
|
||||||
|
Property.Number(label="Temp", configurable=True),
|
||||||
|
Property.Kettle(label="Kettle")])
|
||||||
|
class MashStep(CBPiStep):
|
||||||
|
|
||||||
|
|
||||||
|
async def execute(self):
|
||||||
|
try:
|
||||||
|
kid = self.props.get("Kettle", None)
|
||||||
|
kettle = self.get_kettle(kid)
|
||||||
|
actor = self.get_actor(kettle.get("heater"))
|
||||||
|
print(self.get_actor_state(kettle.get("heater")))
|
||||||
|
await self.cbpi.kettle.set_target_temp(kid, random.randint(0,50))
|
||||||
|
if self.v is True:
|
||||||
|
await self.actor_on(kettle.get("heater"))
|
||||||
|
else:
|
||||||
|
await self.actor_off(kettle.get("heater"))
|
||||||
|
self.v = not self.v
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def setup(cbpi):
|
||||||
|
'''
|
||||||
|
This method is called by the server during startup
|
||||||
|
Here you need to register your plugins at the server
|
||||||
|
|
||||||
|
:param cbpi: the cbpi core
|
||||||
|
:return:
|
||||||
|
'''
|
||||||
|
cbpi.plugin.register("MashStep", MashStep)
|
||||||
|
|
3
cbpi/extension/mashstep/config.yaml
Normal file
3
cbpi/extension/mashstep/config.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
name: MashStep
|
||||||
|
version: 4
|
||||||
|
active: true
|
|
@ -41,6 +41,29 @@
|
||||||
"type": "ActorButton",
|
"type": "ActorButton",
|
||||||
"x": 505,
|
"x": 505,
|
||||||
"y": 140
|
"y": 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3cae292a-f12d-4c9e-8e0e-2fd93a9b253e",
|
||||||
|
"name": "TargetTemp",
|
||||||
|
"props": {
|
||||||
|
"color": "#fff",
|
||||||
|
"kettle": "oHxKz3z5RjbsxfSz6KUgov",
|
||||||
|
"size": "12",
|
||||||
|
"unit": "\u00b0"
|
||||||
|
},
|
||||||
|
"type": "TargetTemp",
|
||||||
|
"x": 160,
|
||||||
|
"y": 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bb90e1ab-7b2d-4623-8df3-3139f91b7087",
|
||||||
|
"name": "Steps",
|
||||||
|
"props": {
|
||||||
|
"width": "200"
|
||||||
|
},
|
||||||
|
"type": "Steps",
|
||||||
|
"x": 595,
|
||||||
|
"y": 50
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pathes": [
|
"pathes": [
|
||||||
|
|
|
@ -2,40 +2,13 @@
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"agitator": "EsmZwWi9Qp3bzmXqq7N3Ly",
|
"agitator": "EsmZwWi9Qp3bzmXqq7N3Ly",
|
||||||
"heater": "YwGzXvWMpmbLb6XobesL8n",
|
"heater": "EsmZwWi9Qp3bzmXqq7N3Ly",
|
||||||
"id": "oHxKz3z5RjbsxfSz6KUgov",
|
"id": "oHxKz3z5RjbsxfSz6KUgov",
|
||||||
"name": "MashTun",
|
"name": "MashTun",
|
||||||
"props": {},
|
"props": {},
|
||||||
"sensor": "8ohkXvFA9UrkHLsxQL38wu",
|
"sensor": "8ohkXvFA9UrkHLsxQL38wu",
|
||||||
"state": {},
|
"state": {},
|
||||||
"target_temp": 52,
|
"target_temp": 25,
|
||||||
"type": "CustomKettleLogic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"agitator": "",
|
|
||||||
"heater": "",
|
|
||||||
"id": "WxAkesrkqiHH3Gywc4fMci",
|
|
||||||
"name": "HLT",
|
|
||||||
"props": {
|
|
||||||
"Param2": "13",
|
|
||||||
"Param3": 1,
|
|
||||||
"Param4": "",
|
|
||||||
"Param5": "8BLRqagLicCdEBDdc77Sgr"
|
|
||||||
},
|
|
||||||
"sensor": "",
|
|
||||||
"state": {},
|
|
||||||
"target_temp": null,
|
|
||||||
"type": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"agitator": "",
|
|
||||||
"heater": "NjammuygecdvMpoGYc3rXt",
|
|
||||||
"id": "a7bWex85Z9Td4atwgazpXW",
|
|
||||||
"name": "Boil",
|
|
||||||
"props": {},
|
|
||||||
"sensor": "",
|
|
||||||
"state": {},
|
|
||||||
"target_temp": 55,
|
|
||||||
"type": "CustomKettleLogic"
|
"type": "CustomKettleLogic"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,43 +4,15 @@
|
||||||
},
|
},
|
||||||
"profile": [
|
"profile": [
|
||||||
{
|
{
|
||||||
"id": "T2y34Mbex9KjNWXhzfCRby",
|
"id": "Gkjdsu45XPcfJimz4yHc4w",
|
||||||
"name": "MashIn",
|
"name": "Test",
|
||||||
"props": {
|
"props": {
|
||||||
"Param1": 123,
|
"Kettle": "oHxKz3z5RjbsxfSz6KUgov",
|
||||||
"Param2": "HALLO",
|
"Temp": "2",
|
||||||
"Param3": 1,
|
"Timer": "1"
|
||||||
"count": 1,
|
|
||||||
"wohoo": 0
|
|
||||||
},
|
},
|
||||||
"status": "P",
|
"status": "P",
|
||||||
"type": "CustomStep2"
|
"type": "MashStep"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "RjS8Zb2GGpUtNsqHsES3yF",
|
|
||||||
"name": "Step2",
|
|
||||||
"props": {
|
|
||||||
"Param1": 123,
|
|
||||||
"Param2": "HALLO",
|
|
||||||
"Param3": 1,
|
|
||||||
"count": 0,
|
|
||||||
"wohoo": 0
|
|
||||||
},
|
|
||||||
"status": "I",
|
|
||||||
"type": "CustomStep2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "WkZG4fDNxZdtZ7uoTsSHhR",
|
|
||||||
"name": "Mash Step 1",
|
|
||||||
"props": {
|
|
||||||
"Param1": 123,
|
|
||||||
"Param2": "HALLO",
|
|
||||||
"Param3": 1,
|
|
||||||
"count": 0,
|
|
||||||
"wohoo": 0
|
|
||||||
},
|
|
||||||
"status": "I",
|
|
||||||
"type": "CustomStep2"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
2
setup.py
2
setup.py
|
@ -32,7 +32,7 @@ setup(name='cbpi',
|
||||||
'shortuuid==1.0.1',
|
'shortuuid==1.0.1',
|
||||||
'tabulate==0.8.7',
|
'tabulate==0.8.7',
|
||||||
'asyncio-mqtt',
|
'asyncio-mqtt',
|
||||||
'cbpi4-ui',
|
'cbpi4ui',
|
||||||
],
|
],
|
||||||
dependency_links=[
|
dependency_links=[
|
||||||
'https://testpypi.python.org/pypi',
|
'https://testpypi.python.org/pypi',
|
||||||
|
|
Loading…
Reference in a new issue