recom 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- recom/__init__.py +6 -0
- recom/__main__.py +25 -0
- recom/backend/__init__.py +0 -0
- recom/backend/uart.py +8 -0
- recom/backend/usb.py +121 -0
- recom/cli.py +47 -0
- recom/device.py +137 -0
- recom/interface.py +50 -0
- recom-0.0.1.dist-info/METADATA +13 -0
- recom-0.0.1.dist-info/RECORD +13 -0
- recom-0.0.1.dist-info/WHEEL +5 -0
- recom-0.0.1.dist-info/entry_points.txt +2 -0
- recom-0.0.1.dist-info/top_level.txt +1 -0
recom/__init__.py
ADDED
recom/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
from recom.cli import cli
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(argv=None):
|
|
8
|
+
"""Main entry point"""
|
|
9
|
+
|
|
10
|
+
if argv==None:
|
|
11
|
+
argv = sys.argv
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
return cli(argv[1:])
|
|
15
|
+
except KeyboardInterrupt:
|
|
16
|
+
print("Aborted by user")
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print("FATAL: ", repr(e))
|
|
19
|
+
print(traceback.format_exc())
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == '__main__':
|
|
24
|
+
sys.exit(main())
|
|
25
|
+
|
|
File without changes
|
recom/backend/uart.py
ADDED
recom/backend/usb.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import usb.core
|
|
3
|
+
import usb.util
|
|
4
|
+
|
|
5
|
+
def find_usb_device_by_vid_pid(vid_pid):
|
|
6
|
+
if isinstance(vid_pid, tuple):
|
|
7
|
+
vid = vid_pid[0]
|
|
8
|
+
pid = vid_pid[1]
|
|
9
|
+
elif isinstance(vid_pid, str):
|
|
10
|
+
if ':' in vid_pid:
|
|
11
|
+
vp_parts = vid_pid.split(':', 1)
|
|
12
|
+
vid = int(vp_parts[0].strip(), 16) if len(vp_parts[0]) else None
|
|
13
|
+
pid = int(vp_parts[1].strip(), 16) if len(vp_parts[1]) else None
|
|
14
|
+
else:
|
|
15
|
+
vid = int(vid_pid.strip(), 16)
|
|
16
|
+
pid = None
|
|
17
|
+
if vid is not None and pid is not None:
|
|
18
|
+
return list(usb.core.find(find_all=True, idVendor=vid, idProduct=pid))
|
|
19
|
+
elif vid is not None:
|
|
20
|
+
return list(usb.core.find(find_all=True, idVendor=vid))
|
|
21
|
+
elif pid is not None:
|
|
22
|
+
return list(usb.core.find(find_all=True, idProduct=pid))
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def find_usb_device_by_serial(serial):
|
|
26
|
+
return usb.core.find(serial=serial)
|
|
27
|
+
|
|
28
|
+
class CTRL_REQ(enum.IntEnum):
|
|
29
|
+
DEVICE_VENDOR_OUT = 0x40
|
|
30
|
+
DEVICE_VENDOR_IN = 0xC0
|
|
31
|
+
INTERFACE_VENDOR_OUT = 0x41
|
|
32
|
+
INTERFACE_VENDOR_IN = 0xC1
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class USBDevice:
|
|
36
|
+
|
|
37
|
+
def __init__(self, dev_handle):
|
|
38
|
+
self.dev = dev_handle
|
|
39
|
+
self.interfaces = []
|
|
40
|
+
|
|
41
|
+
for cfg in self.dev:
|
|
42
|
+
for itf in cfg:
|
|
43
|
+
self.interfaces.append(USBInterface(self.dev, itf))
|
|
44
|
+
self.dev.set_configuration(1)
|
|
45
|
+
|
|
46
|
+
def __repr__(self):
|
|
47
|
+
return "USB Device 0x%04X:0x%04X" % (self.dev.idVendor, self.dev.idProduct)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def dev_type(self):
|
|
51
|
+
return 'usb'
|
|
52
|
+
|
|
53
|
+
def controlRead(self, request, value=0, index=0, dataLen=512, timeout=1000):
|
|
54
|
+
return self.dev.ctrl_transfer(CTRL_REQ.DEVICE_VENDOR_IN, request, value, index, dataLen, timeout)
|
|
55
|
+
|
|
56
|
+
def controlWrite(self, request, data=b'', value=0, index=0, timeout=1000):
|
|
57
|
+
return self.dev.ctrl_transfer(CTRL_REQ.DEVICE_VENDOR_OUT, request, value, index, data, timeout)
|
|
58
|
+
|
|
59
|
+
def get_interface_list(self):
|
|
60
|
+
itf_list = []
|
|
61
|
+
for itf in self.interfaces:
|
|
62
|
+
itf_list.append([itf.desc.bInterfaceClass, itf.desc.bInterfaceSubClass,
|
|
63
|
+
itf.desc.bInterfaceProtocol, itf.itf_str])
|
|
64
|
+
return itf_list
|
|
65
|
+
|
|
66
|
+
def get_interface(self, itf_identifier):
|
|
67
|
+
if isinstance(itf_identifier, int):
|
|
68
|
+
# Interface index
|
|
69
|
+
return self.interfaces[itf_identifier]
|
|
70
|
+
elif isinstance(itf_identifier, tuple):
|
|
71
|
+
# Interface subclass/protocol tuple
|
|
72
|
+
for itf in self.interfaces:
|
|
73
|
+
if itf.desc.bInterfaceSubClass == itf_identifier[0] and \
|
|
74
|
+
itf.desc.bInterfaceProtocol == itf_identifier[1]:
|
|
75
|
+
return itf
|
|
76
|
+
elif isinstance(itf_identifier, str):
|
|
77
|
+
# Interface description string
|
|
78
|
+
for itf in self.interfaces:
|
|
79
|
+
if itf_identifier in itf.itf_str:
|
|
80
|
+
return itf
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class USBInterface():
|
|
85
|
+
|
|
86
|
+
def __init__(self, dev_handle, itf_desc):
|
|
87
|
+
self.dev = dev_handle
|
|
88
|
+
self.desc = itf_desc
|
|
89
|
+
self.itf_idx = itf_desc.bInterfaceNumber
|
|
90
|
+
interface = usb.core.Interface(self.dev, self.itf_idx)
|
|
91
|
+
self.eps = []
|
|
92
|
+
self.ep_in = usb.util.find_descriptor(interface, custom_match = lambda e: \
|
|
93
|
+
usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
94
|
+
usb.util.ENDPOINT_IN)
|
|
95
|
+
self.ep_out = usb.util.find_descriptor(interface, custom_match = lambda e: \
|
|
96
|
+
usb.util.endpoint_direction(e.bEndpointAddress) == \
|
|
97
|
+
usb.util.ENDPOINT_OUT)
|
|
98
|
+
self.subclass = self.desc.bInterfaceSubClass
|
|
99
|
+
self.protocol = self.desc.bInterfaceProtocol
|
|
100
|
+
self.itf_str = usb.util.get_string(self.dev, self.desc.iInterface)
|
|
101
|
+
|
|
102
|
+
def __repr__(self):
|
|
103
|
+
return "%s: Subclass=%d, Protocol=%d, EP_OUT=0x%02X, EP_IN=0x%02X" % \
|
|
104
|
+
(self.itf_str, self.subclass, self.protocol, self.ep_out.bEndpointAddress,
|
|
105
|
+
self.ep_in.bEndpointAddress)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def itf_string(self):
|
|
109
|
+
return self.itf_str
|
|
110
|
+
|
|
111
|
+
def controlRead(self, request, value=0, index=0, dataLen=512, timeout=1000):
|
|
112
|
+
return self.dev.ctrl_transfer(CTRL_REQ.INTERFACE_VENDOR_IN, request, value, index, dataLen, timeout)
|
|
113
|
+
|
|
114
|
+
def controlWrite(self, request, data=b'', value=0, index=0, timeout=1000):
|
|
115
|
+
return self.dev.ctrl_transfer(CTRL_REQ.INTERFACE_VENDOR_OUT, request, value, index, data, timeout)
|
|
116
|
+
|
|
117
|
+
def read(self, dataLen=64, timeout=1000):
|
|
118
|
+
return self.ep_in.read(dataLen, timeout)
|
|
119
|
+
|
|
120
|
+
def write(self, data, timeout=1000):
|
|
121
|
+
return self.ep_out.write(data, timeout)
|
recom/cli.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import recom
|
|
4
|
+
from recom.device import RecomDevice, DeviceException
|
|
5
|
+
|
|
6
|
+
def list_devices(device_id, serial):
|
|
7
|
+
dev = None
|
|
8
|
+
print("Finding device")
|
|
9
|
+
if serial is not None:
|
|
10
|
+
print(f'Find by serial - {serial}')
|
|
11
|
+
try:
|
|
12
|
+
dev = RecomDevice(serial=serial)
|
|
13
|
+
except DeviceException as dev_exp:
|
|
14
|
+
print(dev_exp)
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
else:
|
|
18
|
+
print(f'Find by DeviceID - {device_id}')
|
|
19
|
+
try:
|
|
20
|
+
dev = RecomDevice(device_id=device_id)
|
|
21
|
+
except DeviceException as dev_exp:
|
|
22
|
+
print(dev_exp)
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
print("Found device: %s" % dev)
|
|
26
|
+
|
|
27
|
+
print("\tHW ID/Rev: 0x%04X / 0x%04X" % (dev.hw_id, dev.hw_revision))
|
|
28
|
+
print("\tFW Rev: %s" % dev.fw_revision)
|
|
29
|
+
print("\tSerial: %s" % dev.serial)
|
|
30
|
+
print("\n\tInterfaces:")
|
|
31
|
+
interfaces = dev.getAllInterfaces()
|
|
32
|
+
for itf in interfaces:
|
|
33
|
+
itf_tuple = (itf[1], itf[2])
|
|
34
|
+
itf_handle = dev.getInterfaceHandleFromID(itf_tuple)
|
|
35
|
+
print("\t\t%s" % itf_handle)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cli(argv):
|
|
39
|
+
parser = argparse.ArgumentParser(description="Open a serial port and read/write data.")
|
|
40
|
+
parser.add_argument('--version', action='version', version=recom.__version__,
|
|
41
|
+
help="Print package version")
|
|
42
|
+
parser.add_argument('-d', '--device', help='Device ID to search for ([VID:PID] for USB, port for serial)')
|
|
43
|
+
parser.add_argument('-S', '--serial', help='Serial number to search for')
|
|
44
|
+
|
|
45
|
+
args = parser.parse_args(argv)
|
|
46
|
+
|
|
47
|
+
list_devices(args.device, args.serial)
|
recom/device.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#from interface import DeviceInterface
|
|
2
|
+
import enum
|
|
3
|
+
import struct
|
|
4
|
+
from recom.backend.usb import find_usb_device_by_serial, find_usb_device_by_vid_pid
|
|
5
|
+
from recom.backend.uart import find_serial_device_by_serial, find_serial_device_by_com_port
|
|
6
|
+
from recom.backend.usb import USBDevice
|
|
7
|
+
from recom.interface import RecomInterface
|
|
8
|
+
|
|
9
|
+
class BASE_DEV_CMDS(enum.IntEnum):
|
|
10
|
+
CMD_SCRATCH_REG = 0x00,
|
|
11
|
+
CMD_HW_ID = 0x01,
|
|
12
|
+
CMD_HW_REV = 0x02,
|
|
13
|
+
CMD_FW_REV = 0x03,
|
|
14
|
+
CMD_SERIAL = 0x04,
|
|
15
|
+
CMD_RESET = 0x05
|
|
16
|
+
CMD_GET_INTERFACES = 0x06,
|
|
17
|
+
|
|
18
|
+
class DeviceException(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
class BaseDevice:
|
|
22
|
+
|
|
23
|
+
def __init__(self, device):
|
|
24
|
+
self._interfaces = []
|
|
25
|
+
self._comsBackend = None
|
|
26
|
+
self.dev = device
|
|
27
|
+
#print(self.dev)
|
|
28
|
+
|
|
29
|
+
# TODO: Detect and setup COMs backend
|
|
30
|
+
# For now, default to USB
|
|
31
|
+
self._comsBackend = USBDevice(device)
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
return repr(self._comsBackend)
|
|
35
|
+
|
|
36
|
+
def _detectInterfaces(self):
|
|
37
|
+
#If USB, get the interfaces from the USB library/descriptor
|
|
38
|
+
#If UART or other interface, use the control interface to query the available interfaces
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def getAllInterfaces(self):
|
|
42
|
+
"""Returns a list of available interfaces"""
|
|
43
|
+
return self._comsBackend.get_interface_list()
|
|
44
|
+
|
|
45
|
+
def getInterfaceHandleFromID(self, itf_id):
|
|
46
|
+
"""Finds an interface based on its ID and returns its handle"""
|
|
47
|
+
itf = self._comsBackend.get_interface(itf_id)
|
|
48
|
+
if itf is None:
|
|
49
|
+
raise DeviceException("Interface not found")
|
|
50
|
+
return RecomInterface(self, itf)
|
|
51
|
+
|
|
52
|
+
def getInterfaceHandleFromNumber(self, itf_num):
|
|
53
|
+
"""Finds an interface based on its number in the interface list and returns its handle"""
|
|
54
|
+
itf_list = self._comsBackend.get_interface_list()
|
|
55
|
+
if itf_num >= len(itf_list):
|
|
56
|
+
raise DeviceException("Interface number out of range")
|
|
57
|
+
itf = self._comsBackend.get_interface(itf_list[itf_num])
|
|
58
|
+
return RecomInterface(self, itf)
|
|
59
|
+
|
|
60
|
+
def getHwID(self):
|
|
61
|
+
# The HW ID is a 32-bit number
|
|
62
|
+
data = self._comsBackend.controlRead(BASE_DEV_CMDS.CMD_HW_ID)
|
|
63
|
+
return struct.unpack('<I', data)
|
|
64
|
+
|
|
65
|
+
def getHwRev(self):
|
|
66
|
+
# The HW revision is a 32-bit number
|
|
67
|
+
data = self._comsBackend.controlRead(BASE_DEV_CMDS.CMD_HW_REV)
|
|
68
|
+
return struct.unpack('<I', data)
|
|
69
|
+
|
|
70
|
+
def getFwRev(self):
|
|
71
|
+
# The FW revision is a string
|
|
72
|
+
data = self._comsBackend.controlRead(BASE_DEV_CMDS.CMD_FW_REV)
|
|
73
|
+
return ''.join(chr(x) for x in data)
|
|
74
|
+
|
|
75
|
+
def getSerial(self):
|
|
76
|
+
# The device serial number is a string
|
|
77
|
+
data = self._comsBackend.controlRead(BASE_DEV_CMDS.CMD_SERIAL)
|
|
78
|
+
return ''.join(chr(x) for x in data)
|
|
79
|
+
|
|
80
|
+
def getScratchReg(self):
|
|
81
|
+
# The scratch register is a 32-bit number
|
|
82
|
+
data = self._comsBackend.controlRead(BASE_DEV_CMDS.CMD_SCRATCH_REG)
|
|
83
|
+
return struct.unpack('<I', data)
|
|
84
|
+
|
|
85
|
+
def setScratchReg(self, scratch_value:int):
|
|
86
|
+
data = struct.pack('<I', scratch_value)
|
|
87
|
+
return self._comsBackend.controlWrite(BASE_DEV_CMDS.CMD_SCRATCH_REG, data)
|
|
88
|
+
|
|
89
|
+
class RecomDevice(BaseDevice):
|
|
90
|
+
|
|
91
|
+
def __init__(self, serial=None, device_id=None):
|
|
92
|
+
dev = self._find_device(serial, device_id)
|
|
93
|
+
if dev is None:
|
|
94
|
+
raise DeviceException("No device found")
|
|
95
|
+
super().__init__(dev)
|
|
96
|
+
|
|
97
|
+
def _find_device(self, serial, device_id):
|
|
98
|
+
if serial is not None:
|
|
99
|
+
# Check if we have a USB device with the specified serial
|
|
100
|
+
dev = find_usb_device_by_serial(serial)
|
|
101
|
+
if dev is not None:
|
|
102
|
+
return dev
|
|
103
|
+
# Next, check if there is a serial device with the specified serial
|
|
104
|
+
dev = find_serial_device_by_serial(serial)
|
|
105
|
+
return dev
|
|
106
|
+
elif device_id is not None:
|
|
107
|
+
# Check if we can find a USB device with the specified device_id (VID:PID in this case)
|
|
108
|
+
dev = find_usb_device_by_vid_pid(device_id)
|
|
109
|
+
if len(dev) > 1:
|
|
110
|
+
raise DeviceException("More than one device found!")
|
|
111
|
+
elif len(dev) == 1:
|
|
112
|
+
return dev[0]
|
|
113
|
+
# No USB devices found, now try to find serial devices with the specified device_id (port ID)
|
|
114
|
+
dev = find_serial_device_by_com_port(device_id)
|
|
115
|
+
return dev
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def reset(self):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def hw_id(self):
|
|
123
|
+
return self.getHwID()[0]
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def hw_revision(self):
|
|
127
|
+
return self.getHwRev()[0]
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def fw_revision(self):
|
|
131
|
+
return self.getFwRev()
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def serial(self):
|
|
135
|
+
return self.getSerial()
|
|
136
|
+
|
|
137
|
+
|
recom/interface.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
class RecomInterface:
|
|
4
|
+
|
|
5
|
+
def __init__(self, deviceHandle, interfaceHandle):
|
|
6
|
+
self._parentDevice = deviceHandle
|
|
7
|
+
self._handle = interfaceHandle
|
|
8
|
+
|
|
9
|
+
def __repr__(self):
|
|
10
|
+
return f"Interface: {self.name} (Sub={self._handle.subclass}/Prot={self._handle.protocol})"
|
|
11
|
+
|
|
12
|
+
def read(self, dataLen=-1):
|
|
13
|
+
"""
|
|
14
|
+
Reads data from the interface's data endpoint
|
|
15
|
+
|
|
16
|
+
dataLen specifies how many bytes should be read. If not speciied, any
|
|
17
|
+
available bytes will be returned
|
|
18
|
+
"""
|
|
19
|
+
if dataLen == -1:
|
|
20
|
+
return self._handle.read()
|
|
21
|
+
else:
|
|
22
|
+
return self._handle.read(dataLen)
|
|
23
|
+
|
|
24
|
+
def write(self, data):
|
|
25
|
+
"""
|
|
26
|
+
Writes data to the interface's data endpoint
|
|
27
|
+
|
|
28
|
+
Returns the number of bytes written to the interface
|
|
29
|
+
"""
|
|
30
|
+
return self._handle.write(data)
|
|
31
|
+
|
|
32
|
+
def controlRead(self, request, value=0, index=0, dataLen=64, timeout=1000):
|
|
33
|
+
"""Reads data from the interface's control endpoint"""
|
|
34
|
+
return self._handle.controlRead(request=request,
|
|
35
|
+
value=value,
|
|
36
|
+
index=index,
|
|
37
|
+
dataLen=dataLen,
|
|
38
|
+
timeout=timeout)
|
|
39
|
+
|
|
40
|
+
def controlWrite(self, request, data=b'', value=0, index=0, timeout=1000):
|
|
41
|
+
"""Writes data to the interface's control endpoint"""
|
|
42
|
+
return self._handle.controlWrite(request=request,
|
|
43
|
+
data=data,
|
|
44
|
+
value=value,
|
|
45
|
+
index=index,
|
|
46
|
+
timeout=timeout)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def name(self):
|
|
50
|
+
return self._handle.itf_string
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: recom
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Embedded communication backend
|
|
5
|
+
Home-page: https://github.com/redhill-embedded/recom.git
|
|
6
|
+
Author: Adrian Rothenbuhler
|
|
7
|
+
Author-email: adrian@redhill-embedded.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: embedded communication backedn usb serial
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Requires-Dist: pyusb
|
|
12
|
+
Requires-Dist: pyserial
|
|
13
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
recom/__init__.py,sha256=qKz9D0bpUY1veQV50yDG5zefBZW34Z0X9JjhLk9-FqM,248
|
|
2
|
+
recom/__main__.py,sha256=6PAkcjlgShgd-tb0CPgBQ_piqLAliD2ut4_b3ZVAszI,424
|
|
3
|
+
recom/cli.py,sha256=kAY52PqNIbbbU91F_PGU4JwFtR3Wb29rAISAj1PqbxE,1558
|
|
4
|
+
recom/device.py,sha256=up-1t7a0NHEKl7od6zGMbzYIZ-viAQ5LdW3vEFQBqVk,4682
|
|
5
|
+
recom/interface.py,sha256=Pgesnm10aL0YXsvbgBvBeZkuWdBPUut42QjpPO0snk8,1785
|
|
6
|
+
recom/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
recom/backend/uart.py,sha256=3hNXg6CRLgjhnJebeh5kDXfvVANcNR1Xj8AcdQrEh30,166
|
|
8
|
+
recom/backend/usb.py,sha256=PQEIjbzt8U9WT-YWeTCoYb_KsO32uF4h0QJ33rcE38Y,4734
|
|
9
|
+
recom-0.0.1.dist-info/METADATA,sha256=UlzKWL4w9WaM5MZITVAEjfEkdTzB1Bo9NypOC_qaVI0,350
|
|
10
|
+
recom-0.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
11
|
+
recom-0.0.1.dist-info/entry_points.txt,sha256=HilsJbY9JIp1JI9ODFJFZAzVaENiqCNjQAhsAJYqSG8,46
|
|
12
|
+
recom-0.0.1.dist-info/top_level.txt,sha256=ROCpXv7wHDp6Gpq-HyKPmH3U0Iu9_gMJz7F23mF1k7U,6
|
|
13
|
+
recom-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recom
|