diff --git a/accloudtant/aws/instance.py b/accloudtant/aws/instance.py
index f360c03..02ca135 100644
--- a/accloudtant/aws/instance.py
+++ b/accloudtant/aws/instance.py
@@ -94,11 +94,11 @@ class Instance(object):
     def match_reserved_instance(self, reserved):
         return not (
            self.state != 'running' or
-           reserved['State'] != 'active' or
-           reserved['InstancesLeft'] == 0 or
-           reserved['ProductDescription'] != self.operating_system or
-           reserved['InstanceType'] != self.size or
-           reserved['AvailabilityZone'] != self.availability_zone
+           reserved.state != 'active' or
+           reserved.instances_left == 0 or
+           reserved.product_description != self.operating_system or
+           reserved.instance_type != self.size or
+           reserved.az != self.availability_zone
         )
 
 
diff --git a/accloudtant/aws/reports.py b/accloudtant/aws/reports.py
index e8f2fc9..bcfe9c0 100644
--- a/accloudtant/aws/reports.py
+++ b/accloudtant/aws/reports.py
@@ -17,6 +17,7 @@ import boto3
 from botocore import exceptions
 from tabulate import tabulate
 from accloudtant.aws.instance import Instance
+from accloudtant.aws.reserved_instance import ReservedInstance
 from accloudtant.aws.prices import Prices
 import sys
 
@@ -39,10 +40,12 @@ class Reports(object):
                 for i in ec2.instances.filter(Filters=instances_filters)
             ]
             # self.instances = [Instance(i) for i in ec2.instances.all()]
-            self.reserved_instances = ec2_client.\
-                describe_reserved_instances(
+            self.reserved_instances = [
+                ReservedInstance(i)
+                for i in ec2_client.describe_reserved_instances(
                     Filters=reserved_instances_filters
-                )
+                )['ReservedInstances']
+            ]
             # self.reserved_instances = ec2_client
             # .describe_reserved_instances()
         except exceptions.NoCredentialsError:
@@ -60,13 +63,11 @@ class Reports(object):
                 instance.current = 0.0
             instance_all_upfront = instance_size['ri']['yrTerm3']['allUpfront']
             instance.best = float(instance_all_upfront['effectiveHourly'])
-            for reserved in self.reserved_instances['ReservedInstances']:
-                if 'InstancesLeft' not in reserved.keys():
-                    reserved['InstancesLeft'] = reserved['InstanceCount']
+            for reserved in self.reserved_instances:
                 if instance.match_reserved_instance(reserved):
                     instance.reserved = 'Yes'
-                    instance.current = reserved['UsagePrice']
-                    reserved['InstancesLeft'] -= 1
+                    instance.current = reserved.usage_price
+                    reserved.link(instance)
                     break
 
     def __repr__(self):
diff --git a/accloudtant/aws/reserved_instance.py b/accloudtant/aws/reserved_instance.py
new file mode 100644
index 0000000..4073a20
--- /dev/null
+++ b/accloudtant/aws/reserved_instance.py
@@ -0,0 +1,86 @@
+
+#   Copyright 2015-2016 See CONTRIBUTORS.md file
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+class ReservedInstance(object):
+    def __init__(self, data):
+        self.reserved_instance = data
+        if data['State'] != 'active':
+            self.instances_left = 0
+        else:
+            self.instances_left = self.instance_count
+
+    @property
+    def id(self):
+        return self.reserved_instance['ReservedInstancesId']
+
+    @property
+    def az(self):
+        return self.reserved_instance['AvailabilityZone']
+
+    @property
+    def instance_type(self):
+        return self.reserved_instance['InstanceType']
+
+    @property
+    def product_description(self):
+        return self.reserved_instance['ProductDescription']
+
+    @property
+    def start(self):
+        return self.reserved_instance['Start']
+
+    @property
+    def end(self):
+        return self.reserved_instance['End']
+
+    @property
+    def state(self):
+        return self.reserved_instance['State']
+
+    @property
+    def duration(self):
+        return self.reserved_instance['Duration']
+
+    @property
+    def offering_type(self):
+        return self.reserved_instance['OfferingType']
+
+    @property
+    def usage_price(self):
+        return self.reserved_instance['UsagePrice']
+
+    @property
+    def fixed_price(self):
+        return self.reserved_instance['FixedPrice']
+
+    @property
+    def currency_code(self):
+        return self.reserved_instance['CurrencyCode']
+
+    @property
+    def recurring_charges(self):
+        return self.reserved_instance['RecurringCharges']
+
+    @property
+    def instance_count(self):
+        return self.reserved_instance['InstanceCount']
+
+    @property
+    def instance_tenancy(self):
+        return self.reserved_instance['InstanceTenancy']
+
+    def link(self, instance):
+        self.instances_left -= 1

Create a Reserved Instance type/class
Reserved instances are currently simple dictionaries. Implementing these as objects might help to use them.
diff --git a/tests/aws/test_instance.py b/tests/aws/test_instance.py
index 6f6d73c..fae2e82 100644
--- a/tests/aws/test_instance.py
+++ b/tests/aws/test_instance.py
@@ -16,6 +16,7 @@ import datetime
 import pytest
 from dateutil.tz import tzutc
 import accloudtant.aws.instance
+from accloudtant.aws.reserved_instance import ReservedInstance
 from conftest import MockEC2Instance
 
 
@@ -261,7 +262,7 @@ def test_match_reserved_instance(benchmark):
             ),
         'console_output': {'Output': 'RHEL Linux', },
     }
-    reserved_instance = {
+    ri_data = {
         'ProductDescription': 'Red Hat Enterprise Linux',
         'InstanceTenancy': 'default',
         'InstanceCount': 1,
@@ -298,31 +299,36 @@ def test_match_reserved_instance(benchmark):
 
     ec2_instance = MockEC2Instance(instance_data)
     instance = accloudtant.aws.instance.Instance(ec2_instance)
-    reserved_instance['InstancesLeft'] = reserved_instance['InstanceCount']
+    reserved_instance = ReservedInstance(ri_data)
 
     assert(instance.match_reserved_instance(reserved_instance))
     benchmark(instance.match_reserved_instance, reserved_instance)
 
-    reserved_instance['State'] = 'pending'
+    ri_data['State'] = 'pending'
+    reserved_instance = ReservedInstance(ri_data)
 
     assert(not instance.match_reserved_instance(reserved_instance))
 
-    reserved_instance['State'] = 'active'
-    reserved_instance['InstancesLeft'] = 0
+    ri_data['State'] = 'active'
+    reserved_instance = ReservedInstance(ri_data)
+    reserved_instance.instances_left = 0
 
     assert(not instance.match_reserved_instance(reserved_instance))
 
-    reserved_instance['InstacesLeft'] = 1
-    reserved_instance['ProductDescription'] = 'Windows'
+    ri_data['ProductDescription'] = 'Windows'
+    reserved_instance = ReservedInstance(ri_data)
+    reserved_instance.instances_left = 1
 
     assert(not instance.match_reserved_instance(reserved_instance))
 
-    reserved_instance['ProductionDescription'] = 'Red Hat Enterprise Linux'
-    reserved_instance['InstaceType'] = 't1.micro'
+    ri_data['ProductionDescription'] = 'Red Hat Enterprise Linux'
+    ri_data['InstaceType'] = 't1.micro'
+    reserved_instance = ReservedInstance(ri_data)
 
     assert(not instance.match_reserved_instance(reserved_instance))
 
-    reserved_instance['InstaceType'] = 'r2.8xlarge'
-    reserved_instance['AvailabilityZone'] = 'us-east-1c'
+    ri_data['InstaceType'] = 'r2.8xlarge'
+    ri_data['AvailabilityZone'] = 'us-east-1c'
+    reserved_instance = ReservedInstance(ri_data)
 
     assert(not instance.match_reserved_instance(reserved_instance))
diff --git a/tests/aws/test_reserved_instance.py b/tests/aws/test_reserved_instance.py
new file mode 100644
index 0000000..9627ebf
--- /dev/null
+++ b/tests/aws/test_reserved_instance.py
@@ -0,0 +1,189 @@
+#   Copyright 2015-2016 See CONTRIBUTORS.md file
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import datetime
+import pytest
+from dateutil.tz import tzutc
+import accloudtant.aws.reserved_instance
+from conftest import MockEC2Instance
+from test_reports import get_future_date
+
+
+def test_retired_ri():
+    az = 'us-east-1b'
+    ri_data = {
+        'ProductDescription': 'Linux/UNIX',
+        'InstanceTenancy': 'default',
+        'InstanceCount': 29,
+        'InstanceType': 'm1.large',
+        'Start': datetime.datetime(
+                2011,
+                6,
+                5,
+                6,
+                20,
+                10,
+                494000,
+                tzinfo=tzutc()
+            ),
+        'RecurringCharges': [],
+        'End': datetime.datetime(
+                2011,
+                6,
+                5,
+                6,
+                20,
+                10,
+                tzinfo=tzutc()
+            ),
+        'CurrencyCode': 'USD',
+        'OfferingType': 'Medium Utilization',
+        'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df1223331f',
+        'FixedPrice': 910.0,
+        'AvailabilityZone': az,
+        'UsagePrice': 0.12,
+        'Duration': 31536000,
+        'State': 'retired',
+    }
+
+    ri = accloudtant.aws.reserved_instance.ReservedInstance(ri_data)
+
+    assert(ri.id == ri_data['ReservedInstancesId'])
+    assert(ri.product_description == ri_data['ProductDescription'])
+    assert(ri.instance_tenancy == ri_data['InstanceTenancy'])
+    assert(ri.instance_count == ri_data['InstanceCount'])
+    assert(ri.instance_type == ri_data['InstanceType'])
+    assert(ri.start == ri_data['Start'])
+    assert(ri.recurring_charges == ri_data['RecurringCharges'])
+    assert(ri.end == ri_data['End'])
+    assert(ri.currency_code == ri_data['CurrencyCode'])
+    assert(ri.offering_type == ri_data['OfferingType'])
+    assert(ri.fixed_price == ri_data['FixedPrice'])
+    assert(ri.az == ri_data['AvailabilityZone'])
+    assert(ri.usage_price == ri_data['UsagePrice'])
+    assert(ri.duration == ri_data['Duration'])
+    assert(ri.state == ri_data['State'])
+    assert(ri.instances_left == 0)
+
+
+def test_active_ri():
+    az = 'us-east-1b'
+    ri_data = {
+        'ProductDescription': 'Linux/UNIX',
+        'InstanceTenancy': 'default',
+        'InstanceCount': 1,
+        'InstanceType': 'm1.large',
+        'Start': datetime.datetime(
+                2011,
+                6,
+                5,
+                6,
+                20,
+                10,
+                494000,
+                tzinfo=tzutc()
+            ),
+        'RecurringCharges': [],
+        'End': get_future_date(),
+        'CurrencyCode': 'USD',
+        'OfferingType': 'Medium Utilization',
+        'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df1223331f',
+        'FixedPrice': 910.0,
+        'AvailabilityZone': az,
+        'UsagePrice': 0.12,
+        'Duration': 31536000,
+        'State': 'active',
+    }
+
+    ri = accloudtant.aws.reserved_instance.ReservedInstance(ri_data)
+
+    assert(ri.id == ri_data['ReservedInstancesId'])
+    assert(ri.product_description == ri_data['ProductDescription'])
+    assert(ri.instance_tenancy == ri_data['InstanceTenancy'])
+    assert(ri.instance_count == ri_data['InstanceCount'])
+    assert(ri.instance_type == ri_data['InstanceType'])
+    assert(ri.start == ri_data['Start'])
+    assert(ri.recurring_charges == ri_data['RecurringCharges'])
+    assert(ri.end == ri_data['End'])
+    assert(ri.currency_code == ri_data['CurrencyCode'])
+    assert(ri.offering_type == ri_data['OfferingType'])
+    assert(ri.fixed_price == ri_data['FixedPrice'])
+    assert(ri.az == ri_data['AvailabilityZone'])
+    assert(ri.usage_price == ri_data['UsagePrice'])
+    assert(ri.duration == ri_data['Duration'])
+    assert(ri.state == ri_data['State'])
+    assert(ri.instances_left == ri_data['InstanceCount'])
+
+
+def test_ri_link():
+    az = 'us-east-1b'
+    ri_data = {
+        'ProductDescription': 'Linux/UNIX',
+        'InstanceTenancy': 'default',
+        'InstanceCount': 1,
+        'InstanceType': 'm1.large',
+        'Start': datetime.datetime(
+                2015,
+                6,
+                5,
+                6,
+                20,
+                10,
+                494000,
+                tzinfo=tzutc()
+            ),
+        'RecurringCharges': [],
+        'End': get_future_date(),
+        'CurrencyCode': 'USD',
+        'OfferingType': 'Medium Utilization',
+        'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df1223331f',
+        'FixedPrice': 910.0,
+        'AvailabilityZone': az,
+        'UsagePrice': 0.12,
+        'Duration': 31536000,
+        'State': 'active',
+    }
+    instance_data = {
+        'id': 'i-1840273e',
+        'tags': [{
+            'Key': 'Name',
+            'Value': 'app1',
+        }, ],
+        'instance_type': 'm1.large',
+        'placement': {
+            'AvailabilityZone': az,
+        },
+        'state': {
+            'Name': 'running',
+        },
+        'launch_time': datetime.datetime(
+                2015,
+                10,
+                22,
+                14,
+                15,
+                10,
+                tzinfo=tzutc()
+            ),
+        'console_output': {'Output': 'Linux', },
+    }
+
+    ri = accloudtant.aws.reserved_instance.ReservedInstance(ri_data)
+    instance = MockEC2Instance(instance_data)
+
+    assert(ri.instances_left == 1)
+
+    ri.link(instance)
+
+    assert(ri.instances_left == 0)

pip install -e .
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
-e git+https://github.com/ifosch/accloudtant.git@96ca7fbc89be0344db1af0ec2bc9fdecff6380eb#egg=accloudtant
boto3==1.1.4
botocore==1.2.10
click==4.1
docutils==0.21.2
exceptiongroup==1.2.2
futures==2.2.0
iniconfig==2.1.0
jmespath==0.10.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
python-dateutil==2.9.0.post0
requests==2.8.1
six==1.17.0
tabulate==0.7.5
tomli==2.2.1

name: accloudtant
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - boto3==1.1.4
      - botocore==1.2.10
      - click==4.1
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - futures==2.2.0
      - iniconfig==2.1.0
      - jmespath==0.10.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - requests==2.8.1
      - six==1.17.0
      - tabulate==0.7.5
      - tomli==2.2.1
prefix: /opt/conda/envs/accloudtant

tests/aws/test_instance.py::test_instance tests/aws/test_instance.py::test_unnamed_instance tests/aws/test_instance.py::test_guess_os tests/aws/test_reserved_instance.py::test_retired_ri tests/aws/test_reserved_instance.py::test_active_ri tests/aws/test_reserved_instance.py::test_ri_link
accloudtant/aws/reserved_instance.py accloudtant/aws/instance.py accloudtant/aws/reports.py
accloudtant/aws/reserved_instance.py accloudtant/aws/instance.py accloudtant/aws/reports.py