generated from simonw/python-lib-template-repository
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update version Skip imports of any IXP with a non-ISO code
- Loading branch information
Showing
13 changed files
with
469 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
ixp_tracker/migrations/0005_statspercountry_statsperixp_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Generated by Django 5.0.8 on 2024-09-16 13:56 | ||
|
||
import django.db.models.deletion | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('ixp_tracker', '0004_ixpmember_ixpmember_unique_ixp_membership'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='StatsPerCountry', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('country', models.CharField(max_length=2)), | ||
('stats_date', models.DateField()), | ||
('asn_count', models.IntegerField()), | ||
('member_count', models.IntegerField()), | ||
('asns_ixp_member_rate', models.FloatField()), | ||
('total_capacity', models.FloatField()), | ||
], | ||
options={ | ||
'verbose_name': 'Per-country stats', | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='StatsPerIXP', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('stats_date', models.DateField()), | ||
('capacity', models.FloatField()), | ||
('members', models.IntegerField()), | ||
('local_asns_members_rate', models.FloatField()), | ||
], | ||
options={ | ||
'verbose_name': 'IXP stats', | ||
}, | ||
), | ||
migrations.AddConstraint( | ||
model_name='statspercountry', | ||
constraint=models.UniqueConstraint(fields=('country', 'stats_date'), name='ixp_tracker_unique_per_country_stats'), | ||
), | ||
migrations.AddField( | ||
model_name='statsperixp', | ||
name='ixp', | ||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ixp_tracker.ixp'), | ||
), | ||
migrations.AddConstraint( | ||
model_name='statsperixp', | ||
constraint=models.UniqueConstraint(fields=('ixp', 'stats_date'), name='ixp_tracker_unique_ixp_stats'), | ||
), | ||
] |
36 changes: 36 additions & 0 deletions
36
...r/migrations/0006_remove_statspercountry_ixp_tracker_unique_per_country_stats_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Generated by Django 5.0.8 on 2024-09-17 13:23 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('ixp_tracker', '0005_statspercountry_statsperixp_and_more'), | ||
] | ||
|
||
operations = [ | ||
migrations.RemoveConstraint( | ||
model_name='statspercountry', | ||
name='ixp_tracker_unique_per_country_stats', | ||
), | ||
migrations.RenameField( | ||
model_name='asn', | ||
old_name='registration_country', | ||
new_name='registration_country_code', | ||
), | ||
migrations.RenameField( | ||
model_name='ixp', | ||
old_name='country', | ||
new_name='country_code', | ||
), | ||
migrations.RenameField( | ||
model_name='statspercountry', | ||
old_name='country', | ||
new_name='country_code', | ||
), | ||
migrations.AddConstraint( | ||
model_name='statspercountry', | ||
constraint=models.UniqueConstraint(fields=('country_code', 'stats_date'), name='ixp_tracker_unique_per_country_stats'), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import logging | ||
from datetime import datetime, timezone | ||
from typing import Dict, List, TypedDict, Union | ||
|
||
from django_countries import countries | ||
from django.db.models import Q | ||
|
||
from ixp_tracker.importers import ASNGeoLookup | ||
from ixp_tracker.models import IXP, IXPMember, StatsPerCountry, StatsPerIXP | ||
|
||
logger = logging.getLogger("ixp_tracker") | ||
|
||
|
||
class CountryStats(TypedDict): | ||
all_asns: Union[List[int], None] | ||
member_asns: set | ||
total_capacity: int | ||
|
||
|
||
def generate_stats(geo_lookup: ASNGeoLookup, stats_date: datetime = None): | ||
stats_date = stats_date or datetime.utcnow().replace(tzinfo=timezone.utc) | ||
stats_date = stats_date.replace(day=1) | ||
ixps = IXP.objects.filter(created__lte=stats_date).all() | ||
all_members = (IXPMember.objects | ||
.filter(member_since__lte=stats_date) | ||
.filter(Q(date_left=None) | Q(date_left__gte=stats_date))).all() | ||
all_stats_per_country: Dict[str, CountryStats] = {} | ||
for code, _ in list(countries): | ||
all_stats_per_country[code] = { | ||
"all_asns": None, | ||
"member_asns": set(), | ||
"total_capacity": 0 | ||
} | ||
for ixp in ixps: | ||
logger.debug("Calculating growth stats for IXP", extra={"ixp": ixp.id}) | ||
members = [member for member in all_members if member.ixp == ixp] | ||
member_count = len(members) | ||
capacity = sum(member.speed for member in members) | ||
ixp_country = ixp.country_code | ||
country_stats = all_stats_per_country.get(ixp_country) | ||
if country_stats is None: | ||
logger.warning("Country not found", extra={"country": ixp_country}) | ||
country_stats = { | ||
"all_asns": None, | ||
"member_asns": set(), | ||
"total_capacity": 0 | ||
} | ||
all_stats_per_country[ixp_country] = country_stats | ||
if country_stats.get("all_asns") is None: | ||
all_stats_per_country[ixp_country]["all_asns"] = geo_lookup.get_asns_for_country(ixp_country, stats_date) | ||
member_asns = [member.asn.number for member in members] | ||
local_asns_members_rate = calculate_local_asns_members_rate(member_asns, all_stats_per_country[ixp_country]["all_asns"]) | ||
StatsPerIXP.objects.update_or_create( | ||
ixp=ixp, | ||
stats_date=stats_date.date(), | ||
defaults={ | ||
"ixp": ixp, | ||
"stats_date": stats_date.date(), | ||
"members": member_count, | ||
"capacity": (capacity/1000), | ||
"local_asns_members_rate": local_asns_members_rate, | ||
} | ||
) | ||
# We only count unique ASNs that are members of an IXP in a country | ||
all_stats_per_country[ixp_country]["member_asns"] |= set(member_asns) | ||
# But we count capacity for all members, i.e. an ASN member at 2 IXPs will have capacity at each included in the sum | ||
all_stats_per_country[ixp_country]["total_capacity"] += capacity | ||
for code, _ in list(countries): | ||
country_stats = all_stats_per_country[code] | ||
if country_stats.get("all_asns") is None: | ||
country_stats["all_asns"] = geo_lookup.get_asns_for_country(code, stats_date) | ||
local_asns_members_rate = calculate_local_asns_members_rate(country_stats["member_asns"], country_stats["all_asns"]) | ||
StatsPerCountry.objects.update_or_create( | ||
country_code=code, | ||
stats_date=stats_date.date(), | ||
defaults={ | ||
"asn_count": len(country_stats["all_asns"]), | ||
"member_count": len(country_stats["member_asns"]), | ||
"asns_ixp_member_rate": local_asns_members_rate, | ||
"total_capacity": (country_stats["total_capacity"]/1000), | ||
} | ||
) | ||
|
||
|
||
def calculate_local_asns_members_rate(member_asns: List[int], country_asns: [int]) -> float: | ||
if len(country_asns) == 0: | ||
return 0 | ||
# Ignore the current country for a member ASN (as that might have changed) but just get all current members | ||
# that are in the list of ASNs registered to the country at the time | ||
members_in_country = [asn for asn in member_asns if asn in country_asns] | ||
return len(members_in_country) / len(country_asns) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.