Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.

Commit adb1c27

Browse files
authored
Merge pull request #17 from iotaledger/develop
1.0
2 parents 852d0b3 + b23010a commit adb1c27

File tree

15 files changed

+1281
-170
lines changed

15 files changed

+1281
-170
lines changed

README.rst

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ This is the official Python library for the IOTA Core.
99
It implements both the `official API`_, as well as newly-proposed functionality
1010
(such as signing, bundles, utilities and conversion).
1111

12-
.. warning::
13-
This is pre-release software!
14-
There may be performance and stability issues.
15-
16-
Please report any issues using the `PyOTA Bug Tracker`_.
17-
1812
Join the Discussion
1913
===================
2014
If you want to get involved in the community, need help with getting setup,
@@ -23,6 +17,9 @@ Distributed Ledgers and IoT with other people, feel free to join our `Slack`_.
2317

2418
You can also ask questions on our `dedicated forum`_.
2519

20+
If you encounter any issues while using PyOTA, please report them using the
21+
`PyOTA Bug Tracker`_.
22+
2623
============
2724
Dependencies
2825
============
@@ -33,12 +30,7 @@ Installation
3330
============
3431
To install the latest version::
3532

36-
pip install --pre pyota
37-
38-
**Important:** PyOTA is currently pre-release software.
39-
There may be performance and stability issues.
40-
41-
Please report any issues using the `PyOTA Bug Tracker`_.
33+
pip install pyota
4234

4335
Installing from Source
4436
======================

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
name = 'PyOTA',
2929
description = 'IOTA API library for Python',
3030
url = 'https://github.com/iotaledger/iota.lib.py',
31-
version = '1.0.0b7',
31+
version = '1.0.0',
3232

3333
packages = find_packages('src'),
3434
include_package_data = True,
@@ -52,7 +52,7 @@
5252
license = 'MIT',
5353

5454
classifiers = [
55-
'Development Status :: 4 - Beta',
55+
'Development Status :: 5 - Production/Stable',
5656
'Intended Audience :: Developers',
5757
'License :: OSI Approved :: MIT License',
5858
'Programming Language :: Python :: 2',

src/iota/adapter/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
from abc import ABCMeta, abstractmethod as abstract_method
7+
from collections import deque
78
from inspect import isabstract as is_abstract
89
from socket import getdefaulttimeout as get_default_timeout
910
from typing import Dict, List, Text, Tuple, Union
@@ -312,7 +313,7 @@ def configure(cls, uri):
312313
def __init__(self):
313314
super(MockAdapter, self).__init__()
314315

315-
self.responses = {} # type: Dict[Text, List[dict]]
316+
self.responses = {} # type: Dict[Text, deque]
316317
self.requests = [] # type: List[dict]
317318

318319
def seed_response(self, command, response):
@@ -337,7 +338,7 @@ def seed_response(self, command, response):
337338
# {'message': 'Hello!'}
338339
"""
339340
if command not in self.responses:
340-
self.responses[command] = []
341+
self.responses[command] = deque()
341342

342343
self.responses[command].append(response)
343344
return self
@@ -350,7 +351,7 @@ def send_request(self, payload, **kwargs):
350351
command = payload['command']
351352

352353
try:
353-
response = self.responses[command].pop(0)
354+
response = self.responses[command].popleft()
354355
except KeyError:
355356
raise with_context(
356357
exc = BadApiResponse(

src/iota/api.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -385,63 +385,76 @@ def get_bundles(self, transaction):
385385
"""
386386
return self.getBundles(transaction=transaction)
387387

388-
def get_inputs(self, start=None, end=None, threshold=None):
389-
# type: (Optional[int], Optional[int], Optional[int]) -> dict
388+
def get_inputs(self, start=0, stop=None, threshold=None):
389+
# type: (int, Optional[int], Optional[int]) -> dict
390390
"""
391391
Gets all possible inputs of a seed and returns them with the total
392392
balance.
393393
394394
This is either done deterministically (by generating all addresses
395-
until :py:meth:`find_transactions` returns an empty
396-
result and then doing :py:meth:`get_balances`), or by providing a
397-
key range to search.
395+
until :py:meth:`find_transactions` returns an empty result), or by
396+
providing a key range to search.
398397
399398
:param start:
400399
Starting key index.
400+
Defaults to 0.
401401
402-
:param end:
402+
:param stop:
403403
Stop before this index.
404404
Note that this parameter behaves like the ``stop`` attribute in a
405-
:py:class:`slice` object; the end index is _not_ included in the
405+
:py:class:`slice` object; the stop index is *not* included in the
406406
result.
407407
408-
If not specified, then this method will not stop until it finds
409-
an unused address.
408+
If ``None`` (default), then this method will not stop until it
409+
finds an unused address.
410410
411411
:param threshold:
412-
Determines the minimum threshold for a successful result.
412+
If set, determines the minimum threshold for a successful result:
413413
414414
- As soon as this threshold is reached, iteration will stop.
415415
- If the command runs out of addresses before the threshold is
416416
reached, an exception is raised.
417417
418+
Note that this method does not attempt to "optimize" the result
419+
(e.g., smallest number of inputs, get as close to ``threshold``
420+
as possible, etc.); it simply accumulates inputs in order until
421+
the threshold is met.
422+
423+
If ``threshold`` is 0, the first address in the key range with
424+
a non-zero balance will be returned (if it exists).
425+
426+
If ``threshold`` is ``None`` (default), this method will return
427+
**all** inputs in the specified key range.
428+
418429
:return:
419430
Dict with the following structure::
420431
421432
{
422-
'inputs': [
423-
{
424-
'address': <Address object>,
425-
'balance': <address balance>,
426-
'keyIndex`: <index of the address>,
427-
},
428-
... <one object per input found>
429-
]
430-
433+
'inputs': <list of Address objects>
431434
'totalBalance': <aggregate balance of all inputs>,
432435
}
433436
437+
Note that each Address in the result has its ``balance``
438+
attribute set.
439+
440+
Example::
441+
442+
response = iota.get_inputs(...)
443+
444+
input0 = response['inputs'][0] # type: Address
445+
input0.balance # 42
446+
434447
:raise:
435448
- :py:class:`iota.adapter.BadApiResponse` if ``threshold`` is not
436-
met.
449+
met. Not applicable if ``threshold`` is ``None``.
437450
438451
References:
439452
- https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs
440453
"""
441454
return self.getInputs(
442455
seed = self.seed,
443456
start = start,
444-
end = end,
457+
stop = stop,
445458
threshold = threshold,
446459
)
447460

src/iota/commands/extended/get_inputs.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ def get_response_filter(self):
3434
pass
3535

3636
def _execute(self, request):
37-
end = request['end'] # type: Optional[int]
37+
stop = request['stop'] # type: Optional[int]
3838
seed = request['seed'] # type: Seed
3939
start = request['start'] # type: int
4040
threshold = request['threshold'] # type: Optional[int]
4141

4242
generator = AddressGenerator(seed)
4343

4444
# Determine the addresses we will be scanning.
45-
if end is None:
45+
if stop is None:
4646
# This is similar to the ``getNewAddresses`` command, except it
4747
# is interested in all the addresses that `getNewAddresses`
4848
# skips.
@@ -55,7 +55,7 @@ def _execute(self, request):
5555
else:
5656
break
5757
else:
58-
addresses = generator.get_addresses(start, end - start)
58+
addresses = generator.get_addresses(start, stop)
5959

6060
# Load balances for the addresses that we generated.
6161
gb_response = GetBalancesCommand(self.adapter)(addresses=addresses)
@@ -71,12 +71,7 @@ def _execute(self, request):
7171
addresses[i].balance = balance
7272

7373
if balance:
74-
result['inputs'].append({
75-
'address': addresses[i],
76-
'balance': balance,
77-
'keyIndex': addresses[i].key_index,
78-
})
79-
74+
result['inputs'].append(addresses[i])
8075
result['totalBalance'] += balance
8176

8277
if (threshold is not None) and (result['totalBalance'] >= threshold):
@@ -113,15 +108,15 @@ class GetInputsRequestFilter(RequestFilter):
113108
CODE_INTERVAL_TOO_BIG = 'interval_too_big'
114109

115110
templates = {
116-
CODE_INTERVAL_INVALID: '``start`` must be <= ``end``',
117-
CODE_INTERVAL_TOO_BIG: '``end`` - ``start`` must be <= {max_interval}',
111+
CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``',
112+
CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}',
118113
}
119114

120115
def __init__(self):
121116
super(GetInputsRequestFilter, self).__init__(
122117
{
123118
# These arguments are optional.
124-
'end': f.Type(int) | f.Min(0),
119+
'stop': f.Type(int) | f.Min(0),
125120
'start': f.Type(int) | f.Min(0) | f.Optional(0),
126121
'threshold': f.Type(int) | f.Min(0),
127122

@@ -130,7 +125,7 @@ def __init__(self):
130125
},
131126

132127
allow_missing_keys = {
133-
'end',
128+
'stop',
134129
'start',
135130
'threshold',
136131
}
@@ -143,27 +138,27 @@ def _apply(self, value):
143138
if self._has_errors:
144139
return filtered
145140

146-
if filtered['end'] is not None:
147-
if filtered['start'] > filtered['end']:
141+
if filtered['stop'] is not None:
142+
if filtered['start'] > filtered['stop']:
148143
filtered['start'] = self._invalid_value(
149144
value = filtered['start'],
150145
reason = self.CODE_INTERVAL_INVALID,
151146
sub_key = 'start',
152147

153148
context = {
154149
'start': filtered['start'],
155-
'end': filtered['end'],
150+
'stop': filtered['stop'],
156151
},
157152
)
158-
elif (filtered['end'] - filtered['start']) > self.MAX_INTERVAL:
159-
filtered['end'] = self._invalid_value(
160-
value = filtered['end'],
153+
elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL:
154+
filtered['stop'] = self._invalid_value(
155+
value = filtered['stop'],
161156
reason = self.CODE_INTERVAL_TOO_BIG,
162-
sub_key = 'end',
157+
sub_key = 'stop',
163158

164159
context = {
165160
'start': filtered['start'],
166-
'end': filtered['end'],
161+
'stop': filtered['stop'],
167162
},
168163

169164
template_vars = {

src/iota/commands/extended/get_new_addresses.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from __future__ import absolute_import, division, print_function, \
33
unicode_literals
44

5+
from typing import Optional
6+
57
import filters as f
68

79
from iota.commands import FilterCommand, RequestFilter
@@ -30,12 +32,9 @@ def get_response_filter(self):
3032
pass
3133

3234
def _execute(self, request):
33-
# Optional parameters.
34-
count = request.get('count')
35-
index = request.get('index')
36-
37-
# Required parameters.
38-
seed = request['seed']
35+
count = request['count'] # type: Optional[int]
36+
index = request['index'] # type: int
37+
seed = request['seed'] # type: Seed
3938

4039
generator = AddressGenerator(seed)
4140

@@ -57,7 +56,7 @@ def __init__(self):
5756
{
5857
# ``count`` and ``index`` are optional.
5958
'count': f.Type(int) | f.Min(1),
60-
'index': f.Type(int) | f.Min(0),
59+
'index': f.Type(int) | f.Min(0) | f.Optional(default=0),
6160

6261
'seed': f.Required | Trytes(result_type=Seed),
6362
},

src/iota/commands/extended/get_transfers.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,40 +71,49 @@ def _execute(self, request):
7171

7272
if hashes:
7373
# Sort transactions into tail and non-tail.
74-
tails = set()
75-
non_tails = set()
74+
tail_transaction_hashes = set()
75+
non_tail_bundle_hashes = set()
7676

7777
gt_response = GetTrytesCommand(self.adapter)(hashes=hashes)
78-
transactions = list(map(
78+
all_transactions = list(map(
7979
Transaction.from_tryte_string,
8080
gt_response['trytes'],
81-
))
81+
)) # type: List[Transaction]
8282

83-
for txn in transactions:
83+
for txn in all_transactions:
8484
if txn.is_tail:
85-
tails.add(txn.hash)
85+
tail_transaction_hashes.add(txn.hash)
8686
else:
8787
# Capture the bundle ID instead of the transaction hash so that
8888
# we can query the node to find the tail transaction for that
8989
# bundle.
90-
non_tails.add(txn.bundle_hash)
90+
non_tail_bundle_hashes.add(txn.bundle_hash)
9191

92-
if non_tails:
93-
for txn in self._find_transactions(bundles=non_tails):
92+
if non_tail_bundle_hashes:
93+
for txn in self._find_transactions(bundles=list(non_tail_bundle_hashes)):
9494
if txn.is_tail:
95-
tails.add(txn.hash)
95+
if txn.hash not in tail_transaction_hashes:
96+
all_transactions.append(txn)
97+
tail_transaction_hashes.add(txn.hash)
98+
99+
# Filter out all non-tail transactions.
100+
tail_transactions = [
101+
txn
102+
for txn in all_transactions
103+
if txn.hash in tail_transaction_hashes
104+
]
96105

97106
# Attach inclusion states, if requested.
98107
if inclusion_states:
99108
gli_response = GetLatestInclusionCommand(self.adapter)(
100-
hashes = list(tails),
109+
hashes = list(tail_transaction_hashes),
101110
)
102111

103-
for txn in transactions:
112+
for txn in tail_transactions:
104113
txn.is_confirmed = gli_response['states'].get(txn.hash)
105114

106115
# Find the bundles for each transaction.
107-
for txn in transactions:
116+
for txn in tail_transactions:
108117
gb_response = GetBundlesCommand(self.adapter)(transaction=txn.hash)
109118
txn_bundles = gb_response['bundles'] # type: List[Bundle]
110119

0 commit comments

Comments
 (0)