summaryrefslogtreecommitdiff
path: root/netaddr
diff options
context:
space:
mode:
Diffstat (limited to 'netaddr')
-rwxr-xr-xnetaddr/__init__.py12
-rwxr-xr-xnetaddr/eui/__init__.py5
-rwxr-xr-xnetaddr/ip/__init__.py25
-rw-r--r--netaddr/ip/glob.py30
-rw-r--r--netaddr/ip/intset.py27
-rw-r--r--netaddr/ip/rfc1924.py8
-rwxr-xr-xnetaddr/strategy/__init__.py6
-rwxr-xr-xnetaddr/strategy/eui48.py3
-rwxr-xr-xnetaddr/strategy/eui64.py1
-rwxr-xr-xnetaddr/strategy/ipv4.py1
-rwxr-xr-xnetaddr/strategy/ipv6.py1
-rw-r--r--netaddr/tests/ip/tutorial.txt99
-rw-r--r--netaddr/tests/netaddr_coverage_report.txt20
13 files changed, 153 insertions, 85 deletions
diff --git a/netaddr/__init__.py b/netaddr/__init__.py
index 42d0149..6431007 100755
--- a/netaddr/__init__.py
+++ b/netaddr/__init__.py
@@ -27,8 +27,15 @@ from netaddr.ip.glob import IPGlob, cidr_to_glob, glob_to_cidrs, \
from netaddr.eui import EUI, IAB, OUI
+from netaddr.strategy.ipv4 import valid_str as valid_ipv4
+
+from netaddr.strategy.ipv6 import valid_str as valid_ipv6
+
+from netaddr.strategy.eui48 import mac_eui48, mac_unix, mac_cisco, \
+ mac_bare, mac_pgsql, valid_str as valid_mac
+
from netaddr.strategy.eui48 import mac_eui48, mac_unix, mac_cisco, \
- mac_bare, mac_pgsql
+ mac_bare, mac_pgsql, valid_str as valid_mac
__all__ = [
# Custom exceptions.
@@ -38,7 +45,7 @@ __all__ = [
'EUI', 'IAB', 'OUI',
# MAC address dialect classes.
- 'mac_bare', 'mac_cisco', 'mac_eui48', 'mac_pgsql', 'mac_unix',
+ 'mac_bare', 'mac_cisco', 'mac_eui48', 'mac_pgsql', 'mac_unix', 'valid_mac',
# IP, CIDR and IP range related classes and functions.
'IPAddress', 'IPNetwork', 'IPRange', 'IPSet',
@@ -46,6 +53,7 @@ __all__ = [
'cidr_abbrev_to_verbose', 'cidr_exclude', 'cidr_merge', 'spanning_cidr',
'iter_iprange', 'iprange_to_cidrs','iter_unique_ips',
'smallest_matching_cidr', 'largest_matching_cidr', 'all_matching_cidrs',
+ 'valid_ipv4', 'valid_ipv6',
# IP globbing routines.
'IPGlob', 'valid_glob', 'cidr_to_glob', 'glob_to_cidrs', 'glob_to_iptuple',
diff --git a/netaddr/eui/__init__.py b/netaddr/eui/__init__.py
index e9242d7..02d7248 100755
--- a/netaddr/eui/__init__.py
+++ b/netaddr/eui/__init__.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2008-2009, David P. D. Moss. All rights reserved.
#
@@ -582,9 +581,9 @@ class EUI(object):
A record dict containing IEEE registration details for this EUI
(MAC-48) if available, None otherwise.
"""
- data = {'OUI': self.oui.registration}
+ data = {'OUI': self.oui.registration()}
if self.is_iab():
- data['IAB'] = self.iab.registration
+ data['IAB'] = self.iab.registration()
return DictDotLookup(data)
diff --git a/netaddr/ip/__init__.py b/netaddr/ip/__init__.py
index d0a2734..5835b39 100755
--- a/netaddr/ip/__init__.py
+++ b/netaddr/ip/__init__.py
@@ -16,11 +16,12 @@ from netaddr.strategy import ipv4 as _ipv4, ipv6 as _ipv6
#-----------------------------------------------------------------------------
class BaseIP(object):
"""
- An abstract base class for common operations shared between L{IPAddress}
- and L{IPNetwork}.
+ An abstract base class for common operations shared between various IP
+ related subclasses.
"""
- """Constructor."""
+
def __init__(self):
+ """Constructor."""
self._value = None
self._module = None
@@ -430,6 +431,14 @@ class IPAddress(BaseIP):
return self._module.int_to_packed(self._value)
@property
+ def words(self):
+ """
+ A list of unsigned integer words (octets for IPv4, hextets for IPv6)
+ found in this IP address.
+ """
+ return self._module.int_to_words(self._value)
+
+ @property
def bin(self):
"""
The value of this IP adddress in standard Python binary
@@ -553,7 +562,7 @@ class IPAddress(BaseIP):
class IPNetwork(BaseIP):
"""
An IPv4 or IPv6 network or subnet. A combination of an IP address and a
- network mask address.
+ network mask.
Accepts CIDR and several variants :-
@@ -576,7 +585,7 @@ class IPNetwork(BaseIP):
where 'y' address represent a valid netmask.
- This is like Cisco's ACL (Access Control List) bitmasks.
+ This is like Cisco's ACL bitmasks.
d) Abbreviated CIDR format (as of netaddr 0.7.x this requires
the optional constructor argument C{implicit_prefix=True})::
@@ -1064,7 +1073,7 @@ class IPNetwork(BaseIP):
#-----------------------------------------------------------------------------
class IPRange(BaseIP):
"""
- An arbitrary IP address range.
+ An arbitrary IPv4 or IPv6 address range.
Formed from a lower and upper bound IP address. The upper bound IP cannot
be numerically smaller than the lower bound and the IP version of both
@@ -1651,8 +1660,8 @@ def smallest_matching_cidr(ip, cidrs):
@param cidrs: a sequence of IP addresses and/or subnets.
- @return: the smallest (best) matching IPAddress or IPNetwork object from
- the provided sequence, None if there was no match.
+ @return: the smallest (most specific) matching IPAddress or IPNetwork
+ object from the provided sequence, None if there was no match.
"""
match = None
diff --git a/netaddr/ip/glob.py b/netaddr/ip/glob.py
index bf87b24..5e4de93 100644
--- a/netaddr/ip/glob.py
+++ b/netaddr/ip/glob.py
@@ -125,6 +125,36 @@ def glob_to_iptuple(ipglob):
return IPAddress('.'.join(start_tokens)), IPAddress('.'.join(end_tokens))
#-----------------------------------------------------------------------------
+def glob_to_iprange(ipglob):
+ """
+ A function that accepts a glob-style IP range and returns the equivalent
+ IP range.
+
+ @param ipglob: an IP address range in a glob-style format.
+
+ @return: an IPRange object.
+ """
+ if not valid_glob(ipglob):
+ raise AddrFormatError('not a recognised IP glob range: %r!' % ipglob)
+
+ start_tokens = []
+ end_tokens = []
+
+ for octet in ipglob.split('.'):
+ if '-' in octet:
+ tokens = octet.split('-')
+ start_tokens.append(tokens[0])
+ end_tokens.append(tokens[1])
+ elif octet == '*':
+ start_tokens.append('0')
+ end_tokens.append('255')
+ else:
+ start_tokens.append(octet)
+ end_tokens.append(octet)
+
+ return IPRange('.'.join(start_tokens), '.'.join(end_tokens))
+
+#-----------------------------------------------------------------------------
def iprange_to_globs(start, end):
"""
A function that accepts an arbitrary start and end IP address or subnet
diff --git a/netaddr/ip/intset.py b/netaddr/ip/intset.py
index 5a0004b..f33371b 100644
--- a/netaddr/ip/intset.py
+++ b/netaddr/ip/intset.py
@@ -1,4 +1,3 @@
-# -*- coding: iso-8859-15 -*-
"""Immutable integer set type.
Integer set class.
@@ -507,29 +506,3 @@ class IntSet(object):
if self._max is not _MAXINF:
rv.append("max=%r" % self._max)
return "%s(%s)" % (self.__class__.__name__, ",".join(rv))
-
-if __name__ == "__main__":
- # Little test script demonstrating functionality.
- x = IntSet((10, 20), 30)
- y = IntSet((10, 20))
- z = IntSet((10, 20), 30, (15, 19), min=0, max=40)
- print x
- print x&110
- print x|110
- print x^(15, 25)
- print x-12
- print 12 in x
- print x.issubset(x)
- print y.issubset(x)
- print x.istruesubset(x)
- print y.istruesubset(x)
- for val in x:
- print val
- print x.inverse()
- print x == z
- print x == y
- print x <> y
- print hash(x)
- print hash(z)
- print len(x)
- print x.len()
diff --git a/netaddr/ip/rfc1924.py b/netaddr/ip/rfc1924.py
index cba4e6b..7746c1c 100644
--- a/netaddr/ip/rfc1924.py
+++ b/netaddr/ip/rfc1924.py
@@ -53,11 +53,3 @@ def base85_to_ipv6(addr):
ip = IPAddress(result, 6)
return str(ip)
-
-#-----------------------------------------------------------------------------
-if __name__ == '__main__':
- ip_addr = '1080::8:800:200c:417a'
- print ip_addr
- base85 = ipv6_to_base85(ip_addr)
- print base85
- print base85_to_ipv6(base85)
diff --git a/netaddr/strategy/__init__.py b/netaddr/strategy/__init__.py
index 0d16a80..7861759 100755
--- a/netaddr/strategy/__init__.py
+++ b/netaddr/strategy/__init__.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2008-2009, David P. D. Moss. All rights reserved.
#
@@ -270,8 +269,3 @@ def bin_to_int(bin_val, width):
raise ValueError('not a valid Python binary string: %r!' % bin_val)
return int(bin_val.replace('0b', ''), 2)
-
-if __name__ == '__main__':
- i = 0x100000000
- print hex(i)
- print int_to_bin(i, 32)
diff --git a/netaddr/strategy/eui48.py b/netaddr/strategy/eui48.py
index fb47373..0b0af80 100755
--- a/netaddr/strategy/eui48.py
+++ b/netaddr/strategy/eui48.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2008-2009, David P. D. Moss. All rights reserved.
#
@@ -79,7 +78,7 @@ class mac_unix(mac_eui48):
word_base = 16
class mac_cisco(mac_eui48):
- """A Cisco 'triple hextet' MAC address dialect class.."""
+ """A Cisco 'triple hextet' MAC address dialect class."""
word_size = 16
num_words = width / word_size
word_sep = '.'
diff --git a/netaddr/strategy/eui64.py b/netaddr/strategy/eui64.py
index e563089..94aa508 100755
--- a/netaddr/strategy/eui64.py
+++ b/netaddr/strategy/eui64.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2008-2009, David P. D. Moss. All rights reserved.
#
diff --git a/netaddr/strategy/ipv4.py b/netaddr/strategy/ipv4.py
index f78696d..c8defb4 100755
--- a/netaddr/strategy/ipv4.py
+++ b/netaddr/strategy/ipv4.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2008-2009, David P. D. Moss. All rights reserved.
#
diff --git a/netaddr/strategy/ipv6.py b/netaddr/strategy/ipv6.py
index 4ea7fe8..0a25519 100755
--- a/netaddr/strategy/ipv6.py
+++ b/netaddr/strategy/ipv6.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python
#-----------------------------------------------------------------------------
# Copyright (c) 2008-2009, David P. D. Moss. All rights reserved.
#
diff --git a/netaddr/tests/ip/tutorial.txt b/netaddr/tests/ip/tutorial.txt
index 6d162f0..c276685 100644
--- a/netaddr/tests/ip/tutorial.txt
+++ b/netaddr/tests/ip/tutorial.txt
@@ -74,6 +74,9 @@ You can view an individual IP address in various other formats.
>>> ip.bits()
'11000000.00000000.00000010.00000001'
+>>> ip.words
+(192, 0, 2, 1)
+
}}}
==Representing IP Subnets==
@@ -188,13 +191,21 @@ IPAddress('::')
>>> hex(ip.ip)
'0xfe8000000000000000000000deadbeef'
-Bit-style output isn't as quite as friendly as hexadecimal for such a long numbers, but here the proof that it at least works!
+}}}
+
+Bit-style output isn't as quite as friendly as hexadecimal for such a long numbers, but here the proof that it works!
+
+{{{
>>> ip.ip.bits()
'1111111010000000:0000000000000000:0000000000000000:0000000000000000:0000000000000000:0000000000000000:1101111010101101:1011111011101111'
+}}}
+
Here are some networking details for an IPv6 subnet.
+{{{
+
>>> ip.network, ip.broadcast, ip.netmask, ip.hostmask
(IPAddress('fe80::'), IPAddress('fe80::ffff:ffff:ffff:ffff'), IPAddress('ffff:ffff:ffff:ffff::'), IPAddress('::ffff:ffff:ffff:ffff'))
@@ -204,10 +215,24 @@ Here are some networking details for an IPv6 subnet.
It is likely that with IPv6 becoming more prevalent, you'll want to be able to interoperate between IPv4 and IPv6 address seemlessly.
-Here are a couple of handy methods provided by the IP object to help you to this end.
+Here are a couple of methods that help achieve this.
+
+===IPv4 to IPv6===
{{{
+>>> IPAddress('192.0.2.15').ipv4()
+IPAddress('192.0.2.15')
+
+>>> IPAddress('192.0.2.15').ipv6()
+IPAddress('::ffff:192.0.2.15')
+
+>>> IPAddress('192.0.2.15').ipv6(ipv4_compatible=True)
+IPAddress('::192.0.2.15')
+
+>>> IPAddress('192.0.2.15').ipv6(True)
+IPAddress('::192.0.2.15')
+
>>> ip = IPNetwork('192.0.2.1/23')
>>> ip.ipv4()
@@ -219,6 +244,16 @@ IPNetwork('::ffff:192.0.2.1/119')
>>> ip.ipv6(ipv4_compatible=True)
IPNetwork('::192.0.2.1/119')
+===IPv6 to IPv4===
+
+{{{
+
+>>> IPNetwork('::ffff:192.0.2.1/119').ipv4()
+IPNetwork('192.0.2.1/23')
+
+>>> IPNetwork('::192.0.2.1/119').ipv4()
+IPNetwork('192.0.2.1/23')
+
}}}
Note that the IP object returns IPv4 "mapped" addresses by default in preference to IPv4 "compatible" ones. This has been chosen purposefully as the latter form has been deprecated (see RFC 4291 for details).
@@ -381,7 +416,7 @@ Notice how IPv4 is ordered before IPv6 and overlapping subnets sort in order fro
Another useful operation is the ability to summarize groups of IP subnets and addresses, merging them together where possible to create the smallest possible list of CIDR subnets.
-Here is how to do this using the cidr_merge() function.
+Here is how to do this using the `cidr_merge()` function.
First we create a list of IP objects that is a good mix of individual addresses and subnets, along with some string based IP address values for good measure. To make things more challenging some IPv6 addresses have been included as well.
@@ -417,7 +452,7 @@ The iter_iprange() function allow you to do just this.
It is equally nice to know what the actual list of CIDR subnets is that would correctly cover this non-aligned range of addresses.
-Here is cidr_merge() coming to the rescue.
+Here `cidr_merge()` comes to the rescue.
{{{
@@ -486,22 +521,32 @@ IP addresses fall several broad categories and not all are suitable for assignme
Unicast
+{{{
+
>>> IPAddress('192.0.2.1').is_unicast()
True
>>> IPAddress('fe80::1').is_unicast()
True
+}}}
+
Multicast
+{{{
+
>>> IPAddress('239.192.0.1').is_multicast()
True
>>> IPAddress('ff00::1').is_multicast()
True
+}}}
+
Private
+{{{
+
>>> IPAddress('172.24.0.1').is_private()
True
@@ -511,49 +556,69 @@ True
>>> IPAddress('192.168.0.1').is_private()
True
+}}}
+
Reserved
+{{{
+
>>> IPAddress('253.0.0.1').is_reserved()
True
+}}}
+
Public (Internet) addresses.
Note that not all of these may be allocated by the various regional Internet registrars.
+{{{
+
>>> ip = IPAddress('62.125.24.5')
>>> ip.is_unicast() and not ip.is_private()
True
+}}}
+
There are also other types of addresses that have specific functions e.g. masking
Netmasks
+{{{
+
>>> IPAddress('255.255.254.0').is_netmask()
True
+}}}
+
Hostmasks
+{{{
+
>>> IPAddress('0.0.1.255').is_hostmask()
True
+}}}
+
Loopback addresses
-IPv4
+{{{
>>> IPAddress('127.0.0.1').is_loopback()
True
-IPv6
-
>>> IPAddress('::1').is_loopback()
True
+}}}
+
==IP address comparisons==
IP objects can be compared with each other. As an IP object can represent both an individual IP address and an implicit network, it pays to get both sides of your comparison into the same terms before you compare them to avoid any odd results.
Here are some comparisons of individual IP address to get the ball rolling.
+{{{
+
>>> IPAddress('192.0.2.1') == IPAddress('192.0.2.1')
True
@@ -578,15 +643,23 @@ True
>>> IPAddress('192.0.2.1') <= IPAddress('192.0.2.2')
True
+}}}
+
Now lets try something a little more interesting.
+{{{
+
>>> IPNetwork('192.0.2.0/24') == IPNetwork('192.0.2.112/24')
True
+}}}
+
Hmmmmmmmm... looks a bit odd doesn't it? That's because by default, IP objects compare their subnets (or lower and upper boundaries) rather than their individual IP address values.
The solution to this situation is very simple. Knowing this default behaviour, just be explicit about exactly which portion of each IP object you'd like to compare using pass-through properties.
+{{{
+
>>> IPNetwork('192.0.2.0/24').ip == IPNetwork('192.0.2.112/24').ip
False
@@ -609,25 +682,19 @@ True
>>> IPNetwork('192.0.2.0/24') < IPNetwork('192.0.3.0/24')
True
+}}}
+
==Interaction with DNS==
It is a common administrative task to Generating reverse IP lookups for DNS. This is particularly arduous for IPv6 addresses.
-Here is how you do this using the IP object's reverse_dns() method.
-
-IPv4
+Here is how you do this using the IP object's `reverse_dns()` method.
{{{
>>> IPAddress('172.24.0.13').reverse_dns
'13.0.24.172.in-addr.arpa.'
-}}}
-
-IPv6
-
-{{{
-
>>> IPAddress('fe80::feeb:daed').reverse_dns
'd.e.a.d.b.e.e.f.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa.'
diff --git a/netaddr/tests/netaddr_coverage_report.txt b/netaddr/tests/netaddr_coverage_report.txt
index 575793a..1b378cf 100644
--- a/netaddr/tests/netaddr_coverage_report.txt
+++ b/netaddr/tests/netaddr_coverage_report.txt
@@ -1,20 +1,20 @@
Name Stmts Exec Cover Missing
-------------------------------------------------
-__init__ 13 12 92% 11
-core 65 53 81% 66, 88-89, 98-100, 162, 169, 175-176, 179, 182
-eui/__init__ 297 217 73% 50, 55-57, 68, 109, 131, 171-182, 212-214, 219, 232, 252, 256, 269, 319, 323, 352-354, 359, 364-367, 373, 379, 388, 437-440, 445, 449-468, 472, 480, 495-496, 503-506, 515-516, 523-526, 533-536, 543-546, 607-611
+__init__ 15 14 93% 11
+core 65 55 84% 66, 88-89, 98-100, 162, 175-176, 179
+eui/__init__ 287 210 73% 51, 56-58, 69, 112, 159-170, 201-203, 208, 221, 248, 297, 301, 330-332, 337, 342-345, 351, 357, 366, 415-418, 423, 427-446, 450, 458, 473-474, 481-484, 493-494, 501-504, 511-514, 521-524, 585-589
eui/ieee 117 91 77% 75, 122, 150-151, 201, 227-228, 253-259, 277-292, 297-298
-fbsocket 161 136 84% 47, 50-51, 58-59, 68, 72-74, 112, 132, 136, 156, 166, 182-183, 203, 214-216, 225, 228, 232, 247, 254, 258
-ip/__init__ 723 571 78% 37-38, 49-50, 61-62, 73-74, 85-86, 97-98, 134-137, 140, 151-152, 171-182, 189, 196, 240, 252, 262, 283, 290-291, 297, 309, 323, 353, 369, 396, 413, 435-448, 461-473, 482, 491, 500, 509, 518, 525, 641-643, 648, 655-656, 662, 676, 691, 775, 790-796, 831-833, 843, 883-891, 910, 929-932, 941-944, 957, 984, 989, 999, 1026-1037, 1072, 1099-1123, 1131-1135, 1159-1160, 1172, 1176, 1180, 1191-1193, 1265-1268, 1285-1286, 1310, 1376, 1379, 1384, 1418, 1444-1445, 1484, 1490, 1520, 1525
-ip/glob 106 84 79% 60, 68, 73, 77, 80, 82, 84, 89, 91, 93, 108, 143, 173, 223, 232-234, 237, 240-241, 248, 252
+fbsocket 158 133 84% 47, 50-51, 58-59, 68, 72-74, 109, 129, 133, 153, 163, 179-180, 200, 211-213, 222, 225, 229, 244, 251, 255
+ip/__init__ 762 625 82% 32, 39, 56-57, 68-69, 80-81, 92-93, 104-105, 116-117, 153-156, 159, 170-171, 190-201, 208, 215, 258, 270, 280, 301, 308-309, 315, 327, 341, 371, 387, 414, 431, 466-472, 491, 508, 517, 526, 535, 544, 551, 666-668, 673, 680-681, 687, 701, 716, 800, 815-821, 856-858, 868, 916, 935, 954-957, 966-969, 982, 1009, 1014, 1024, 1051-1062, 1097, 1124-1148, 1156-1160, 1184-1185, 1197, 1201, 1205, 1216-1218, 1290-1293, 1310-1311, 1335, 1401, 1404, 1409, 1443, 1469-1470, 1509, 1515, 1545, 1550, 1669, 1698, 1727
+ip/glob 122 85 69% 60, 68, 73, 77, 80, 82, 84, 89, 91, 93, 108, 137-155, 173, 203, 253, 262-264, 267, 270-271, 278, 282
ip/iana 173 139 80% 68, 72, 76, 80, 91, 362-372, 390, 404-408, 415-432, 438
-ip/intset 289 147 50% 53, 58-60, 64, 69-71, 74-76, 79-81, 84, 119-125, 134, 136, 140, 142, 144, 147, 149, 154, 157, 162, 164, 168, 170, 173, 177, 180, 182, 184, 186, 211, 223-226, 235-251, 272, 293, 305, 393-404, 416, 427-436, 445-452, 457, 465-485, 494-507, 511-533
-ip/rfc1924 31 0 0% 6-63
+ip/intset 289 147 50% 53, 58-60, 64, 69-71, 74-76, 79-81, 84, 119-125, 134, 136, 140, 142, 144, 147, 149, 154, 157, 162, 164, 168, 170, 173, 177, 180, 182, 184, 186, 211, 223-226, 235-251, 272, 293, 306, 394-405, 417, 428-437, 446-453, 458, 466-486, 495-509, 513-535
+ip/rfc1924 31 25 80% 46, 59-63
ip/sets 192 166 86% 28, 35, 75, 152, 162, 191, 206, 217-218, 227-230, 239-242, 252, 277-280, 290, 441-445, 453
strategy/__init__ 116 93 80% 44, 47, 53, 72, 97, 121, 127, 134-137, 154, 191, 207, 210, 215, 222-225, 254, 270, 275-277
strategy/eui48 133 113 84% 139-147, 173, 190, 246-248, 264-266, 283-285, 293
strategy/eui64 72 48 66% 72-79, 94-96, 101, 141-149, 153, 161, 165, 169, 177, 185
-strategy/ipv4 81 65 80% 25-26, 80-87, 97, 103-104, 122, 174, 187, 193
+strategy/ipv4 81 65 80% 25-26, 79-86, 96, 102-103, 121, 173, 186, 192
strategy/ipv6 104 91 87% 20-25, 108, 124, 157-158, 210, 226, 240, 244, 248
-------------------------------------------------
-TOTAL 2673 2026 75%
+TOTAL 2717 2100 77%