Passed
Push — master ( fbe99c...28cd06 )
by Dave
01:06
created

domain.mining   F

Complexity

Total Complexity 102

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 281
dl 0
loc 388
rs 2
c 0
b 0
f 0
wmc 102

42 Methods

Rating   Name   Duplication   Size   Complexity  
A MinerCommand.__init__() 0 3 1
A MinerInfo.__init__() 0 3 1
A Login.__init__() 0 3 1
A MinerCurrentPool.findpoolnumberforpool() 0 8 4
B Miner.status() 0 3 6
A Miner.set_ftp_port() 0 3 3
A Miner.uptime() 0 5 1
B Miner.updatefrom() 0 20 7
A Miner.is_send_offline_alert() 0 5 2
A Miner.isvalid_minerid() 0 2 1
A Miner.formattime() 0 14 5
A Miner.__init__() 0 47 1
A Miner.setminerinfo() 0 7 4
A Miner.monitored() 0 12 4
A Miner.isvalid_networkid() 0 2 1
A MinerApiCall.start() 0 2 1
A Miner.offline_now() 0 3 1
A Miner.pools_available() 0 10 4
A Miner.currentpoolname() 0 5 2
A Miner.isvalid_ipaddress() 0 2 1
A Miner.summary() 0 3 1
A Miner.key() 0 10 4
A Miner.monitorresponsetime() 0 3 2
A Pool.__init__() 0 7 1
A MinerApiCall.elapsed() 0 2 1
A MinerApiCall.stop() 0 2 1
A Miner.should_monitor() 0 16 4
B Pool.create() 0 19 8
A Miner.utc_to_local() 0 2 1
A Pool.is_same_as() 0 2 1
A Miner.hash_or_offline() 0 6 3
A Miner.is_key_updated() 0 3 1
A Miner.online_now() 0 3 1
A Miner.is_unknown() 0 3 1
A AvailablePool.key() 0 3 1
B Miner.create() 0 19 8
A Miner.can_monitor() 0 6 3
A Miner.is_manually_disabled() 0 4 2
A Miner.is_disabled() 0 4 3
A MinerCurrentPool.__init__() 0 7 1
A MinerApiCall.__init__() 0 5 1
A AvailablePool.__init__() 0 7 1

How to fix   Complexity   

Complexity

Complex classes like domain.mining often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
'''#Full Cycle Mining Domain'''
2
import time
3
from datetime import datetime, timezone
4
5
class MinerStatus:
6
    '''Status of Miner'''
7
    Online = 'online'
8
    Offline = 'offline'
9
    Disabled = 'disabled'
10
11
class MinerAccessLevel:
12
    Restricted = 'restricted'
13
    Privileged = 'priviledged'
14
    #not really a level, waiting for access upgrade
15
    Waiting = 'waiting'
16
17
class Login(object):
18
    """Login name and password for access to miners"""
19
    def __init__(self, username, password):
20
        self.username = username
21
        self.password = password
22
23
class MinerInfo(object):
24
    '''Meta information about a miner
25
    type and algo
26
    '''
27
    def __init__(self, miner_type, minerid):
28
        self.miner_type = miner_type
29
        self.minerid = minerid
30
31
class MinerCommand(object):
32
    """Command that could be sent to a miner"""
33
    def __init__(self, command='', parameter=''):
34
        self.command = command
35
        self.parameter = parameter
36
37
class Miner(object):
38
    """Miner"""
39
40
    def __init__(self, name, status=MinerStatus.Online, miner_type='', ipaddress='',
41
                 port='', ftpport='', username='', password='', clientid='', networkid='',
42
                 minerid='', lastmonitor=None, offlinecount=0, defaultpool='', minerinfo=None,
43
                 minerpool=None, minerstats=None, laststatuschanged=None,
44
                 in_service_date=None, location=None):
45
        #friendly name for your miner
46
        self.name = name
47
        self._status = status
48
        #saved or derived from monitoring? type of miner. Antminer S9, Antminer D3, etc.
49
        self.miner_type = miner_type
50
        #ip address, usuall will be local ip address. example: 192.168.x.y
51
        self.ipaddress = ipaddress
52
        #ip port, usually will be 4028
53
        self.port = port
54
        self.ftpport = ftpport
55
        self.username = username
56
        self.password = password
57
        #the mydevices clientid for device
58
        self.clientid = clientid
59
        #network identifier for miner. usually the macaddress
60
        self.networkid = networkid
61
        #so far have only seen Antminer S9 have the minerid from STATS command
62
        self.minerid = minerid
63
        #last time the miner was monitored
64
        self.lastmonitor = lastmonitor
65
        self.monitorcount = 0
66
        self.monitortime = 0
67
        #number of times miner is offline during this session
68
        self.offlinecount = offlinecount
69
        #name of the pool that the miner should default to when it is provisioned
70
        self.defaultpool = defaultpool
71
        #meta info on the miner. should be assigned during discovery and monitor
72
        self.minerinfo = minerinfo
73
        #MinerCurrentPool
74
        self.minerpool = minerpool
75
        #MinerStatistics
76
        self.minerstats = minerstats
77
        #status of the miner. online, offline,disabled etc
78
        self.laststatuschanged = laststatuschanged
79
        #store is where the object was stored. mem is for memcache.
80
        self.store = ''
81
        # the date that the miner was put into service or first discovered
82
        self.in_service_date = in_service_date
83
        # location of miner. could be name of facility or rack
84
        self.location = location
85
        #save a copy of the original key so we can detect if it changed
86
        self.key_original = self.key()
87
88
    @classmethod
89
    def create(cls, values):
90
        '''create entity from values dict'''
91
        miner = Miner('', '', '', '', '', '', '')
92
        #todo: find pythonic way to do this
93
        for pair in values:
94
            if 'minerid' in pair:
95
                miner.minerid = pair['minerid']
96
            if 'name' in pair:
97
                miner.name = pair['name']
98
            if 'ipaddress' in pair:
99
                miner.ipaddress = pair['ipaddress']
100
            if 'port' in pair:
101
                miner.port = pair['port']
102
            if 'location' in pair:
103
                miner.location = pair['location']
104
            if 'in_service_date' in pair:
105
                miner.in_service_date = pair['in_service_date']
106
        return miner
107
108
    @property
109
    def is_key_updated(self):
110
        return self.key_original != self.key()
111
112
    @property
113
    def status(self):
114
        return self._status
115
116
    @status.setter
117
    def status(self, value):
118
        if value != '' and value != MinerStatus.Online and value != MinerStatus.Offline and value != MinerStatus.Disabled:
119
            raise ValueError('Invalid miner status {0}'.format(value))
120
        if self._status != value:
121
            self.laststatuschanged = datetime.utcnow()
122
        self._status = value
123
124
    @property
125
    def pools_available(self):
126
        if self.minerpool is None:
127
            return None
128
        available = []
129
        if 'POOLS' in self.minerpool.allpools:
130
            jpools = self.minerpool.allpools['POOLS']
131
            for jpool in jpools:
132
                available.append(AvailablePool(pool_type=self.miner_type, named_pool=None, url=jpool['URL'], user=jpool['User'], priority=jpool['Priority']))
133
        return available
134
135
    #@property
136
    def key(self):
137
        '''cache key for this entity'''
138
        thekey = self.name
139
        if self.isvalid_minerid():
140
            thekey = self.minerid
141
        elif self.isvalid_networkid():
142
            thekey = str(self.networkid)
143
        elif self.isvalid_ipaddress():
144
            thekey = '{0}:{1}'.format(self.ipaddress, self.port)
145
        return thekey
146
147
    @property
148
    def is_unknown(self):
149
        return self.minerid == 'unknown'
150
151
    def isvalid_minerid(self):
152
        return self.minerid is not None and self.minerid and not self.is_unknown
153
154
    def isvalid_networkid(self):
155
        return self.networkid is not None and self.networkid and str(self.networkid) != '{}'
156
157
    def isvalid_ipaddress(self):
158
        return self.ipaddress is not None and self.ipaddress
159
160
    def set_ftp_port(self, port):
161
        if self.ftpport is not None and self.ftpport: return
162
        self.ftpport = port
163
164
    #todo: move ui code out of entity
165
    def summary(self):
166
        #datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S%f%z')
167
        return '{0} {1} {2} {3}'.format(self.name, self.hash_or_offline(), self.formattime(self.lastmonitor), self.currentpoolname())
168
169
    def currentpoolname(self):
170
        if self.minerpool is None:
171
            return '?'
172
        #todo:look up pools here?
173
        return self.minerpool.poolname
174
175
    def hash_or_offline(self):
176
        '''hash or offline status of miner'''
177
        if self.status != MinerStatus.Online:
178
            return self.status
179
        if self.minerstats is None: return self.status
180
        return self.minerstats.stats_summary()
181
182
    #todo: move to appservice
183
    def utc_to_local(self, utc_dt):
184
        return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)
185
186
    def formattime(self, ptime):
187
        '''format time'''
188
        if ptime is None:
189
            return ''
190
        if isinstance(ptime, datetime):
191
            return self.utc_to_local(ptime).strftime('%m-%d %H:%M:%S')
192
        stime = ptime
193
        if '.' in stime:
194
            stime = stime[0:stime.index('.') - 1]
195
        try:
196
            parsedtime = datetime.strptime(stime, '%Y-%m-%dT%H:%M:%S')
197
            return self.utc_to_local(parsedtime).strftime('%m-%d %H:%M:%S')
198
        except ValueError:
199
            return stime
200
201
    def uptime(self, seconds):
202
        minutes, _ = divmod(seconds, 60)
203
        hours, minutes = divmod(minutes, 60)
204
        days, hours = divmod(hours, 24)
205
        return "%dd%dh%02dm" % (days, hours, minutes)
206
207
    def is_disabled(self):
208
        if self.is_manually_disabled() or self.status == MinerStatus.Disabled:
209
            return True
210
        return False
211
212
    def is_manually_disabled(self):
213
        if self.name.startswith("#"):
214
            return True
215
        return False
216
217
    def can_monitor(self):
218
        if not self.ipaddress:
219
            return False
220
        if not self.port:
221
            return False
222
        return True
223
224
    def should_monitor(self):
225
        #always monitor at least once when fcm app starts up
226
        if self.lastmonitor is None:
227
            return True
228
        #no need to monitor if manually disabled
229
        if self.is_manually_disabled():
230
            return False
231
        if self.is_disabled():
232
            #keep monitoring if it was us that disabled the miner
233
            #need to keep monitoring (at longer interval) so we can detect when comes online
234
            #if its a planned outage then user should manually disable to stop monitoring
235
            #since = (datetime.utcnow() - self.lastmonitor).total_seconds()
236
            #if since > 10 * 60:
237
            return True
238
            #return False
239
        return True
240
241
    def offline_now(self):
242
        self.status = MinerStatus.Offline
243
        self.offlinecount += 1
244
245
    def online_now(self):
246
        self.status = MinerStatus.Online
247
        self.offlinecount = 0
248
249
    def is_send_offline_alert(self):
250
        #todo: make configurable
251
        if self.offlinecount <= 3:
252
            return True
253
        return False
254
255
    def monitored(self, stats, pool=None, info=None, sec=None):
256
        if stats:
257
            self.lastmonitor = datetime.utcnow()
258
            self.status = MinerStatus.Online
259
        if sec is not None:
260
            self.monitorcount += 1
261
            self.monitortime += sec
262
        #todo: process stats and pool
263
        self.setminerinfo(info)
264
        if pool is not None:
265
            self.minerpool = pool
266
        self.minerstats = stats
267
268
    def monitorresponsetime(self):
269
        if self.monitorcount == 0: return 0
270
        return self.monitortime/self.monitorcount
271
272
    def setminerinfo(self, info):
273
        if info is not None:
274
            self.minerinfo = info
275
            if not self.miner_type:
276
                self.miner_type = info.miner_type
277
            if not self.minerid:
278
                self.minerid = info.minerid
279
280
    def updatefrom(self, updatedminer):
281
        if self.minerid != updatedminer.minerid and self.name != updatedminer.name:
282
            return
283
        if self.minerid == updatedminer.minerid and self.name != updatedminer.name:
284
            self.name = updatedminer.name
285
        self.setminerinfo(updatedminer.minerinfo)
286
287
        #self.minerid = updatedminer.minerid
288
        fields = ['lastmonitor', 'status', 'ipaddress', 'port', 'username', 'password', 'clientid']
289
        fields.append('offlinecount')
290
        fields.append('defaultpool')
291
        fields.append('minerpool')
292
        fields.append('minerstats')
293
        fields.append('networkid')
294
        fields.append('location')
295
        fields.append('in_service_date')
296
        for fld in fields:
297
            val = getattr(updatedminer, fld)
298
            if val:
299
                setattr(self, fld, val)
300
301
class AvailablePool(object):
302
    """A pool available on a miner
303
    pool_type is the miner type (e.g. Antminer S9)
304
    """
305
306
    def __init__(self, pool_type, named_pool=None, url='', user='', password='x', priority=None):
307
        self.pool_type = pool_type
308
        self.named_pool = named_pool
309
        self.url = url
310
        self.user = user
311
        self.password = password
312
        self.priority = priority
313
314
    @property
315
    def key(self):
316
        return '{0}|{1}'.format(self.url, self.user)
317
318
class MinerApiCall(object):
319
    '''info about one call to miner'''
320
    def __init__(self, miner: Miner):
321
        self.miner = miner
322
        self.when = datetime.now()
323
        self.start_time = None
324
        self.stop_time = None
325
326
    def start(self):
327
        self.start_time = time.perf_counter()
328
    def stop(self):
329
        self.stop_time = time.perf_counter()
330
    def elapsed(self):
331
        return self.stop_time - self.start_time
332
333
class Pool(object):
334
    """A configured (Named) Pool.
335
    Does not have to be attached to miner yet
336
    """
337
338
    def __init__(self, pool_type, name, url, user, priority, password='x'):
339
        self.pool_type = pool_type
340
        self.name = name
341
        self.url = url
342
        self.user = user
343
        self.priority = priority
344
        self.password = password
345
346
    @classmethod
347
    def create(cls, values):
348
        '''create entity from values dict'''
349
        entity = Pool('', '', '', '', '')
350
        #todo: find pythonic way to do this
351
        for pair in values:
352
            if 'pool_type' in pair:
353
                entity.pool_type = pair['pool_type']
354
            if 'name' in pair:
355
                entity.name = pair['name']
356
            if 'url' in pair:
357
                entity.url = pair['url']
358
            if 'user' in pair:
359
                entity.user = pair['user']
360
            if 'priority' in pair:
361
                entity.priority = pair['priority']
362
            if 'password' in pair:
363
                entity.password = pair['password']
364
        return entity
365
366
367
    def is_same_as(self, available_pool: AvailablePool):
368
        return available_pool.url == self.url and available_pool.user.startswith(self.user)
369
370
class MinerCurrentPool(object):
371
    '''The current pool where a miner is mining'''
372
    def __init__(self, miner, currentpool=None, currentworker=None, allpools=None):
373
        self.miner = miner
374
        self.poolname = '?'
375
        self.currentpool = currentpool
376
        self.currentworker = currentworker
377
        #allpools is a json object
378
        self.allpools = allpools
379
380
    def findpoolnumberforpool(self, url, worker):
381
        jpools = self.allpools["POOLS"]
382
        for pool in jpools:
383
            thisurl = pool["URL"]
384
            thisworker = pool["User"]
385
            if thisurl == url and thisworker.startswith(worker):
386
                return pool["POOL"]
387
        return None
388