From f5bd92893fc438537090f173103a3b0d5b884940 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 27 Feb 2024 13:05:49 -0800 Subject: [PATCH 01/69] change target to use abstract base model --- tom_targets/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index a6d2bc4cd..412e7cfc3 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -66,7 +66,7 @@ def make_simple_name(self, name): return name.lower().replace(" ", "").replace("-", "").replace("_", "").replace("(", "").replace(")", "") -class Target(models.Model): +class BaseTarget(models.Model): """ Class representing a target in a TOM @@ -273,6 +273,9 @@ class Target(models.Model): except (ImportError, AttributeError): matches = TargetMatchManager() + class Meta: + abstract = True + @transaction.atomic def save(self, *args, **kwargs): """ @@ -420,6 +423,10 @@ def give_user_access(self, user): assign_perm('targets.delete_target', user, self) +class Target(BaseTarget): + pass + + class TargetName(models.Model): """ Class representing an alternative name for a ``Target``. From 332bbac248de4d5831479cd6731bed2a0f5a5db1 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 27 Feb 2024 17:15:23 -0800 Subject: [PATCH 02/69] allow dynamic Target Base Models --- tom_targets/models.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index 412e7cfc3..9b0d695f5 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -423,7 +423,24 @@ def give_user_access(self, user): assign_perm('targets.delete_target', user, self) -class Target(BaseTarget): +def get_abstract_target_base_models(): + base_classes = (BaseTarget,) + try: + BASE_TARGET_MODELS = settings.BASE_TARGET_MODELS + except AttributeError: + return base_classes + + for model in BASE_TARGET_MODELS: + try: + clazz = import_string(model) + except (ImportError, AttributeError): + raise ImportError(f'Could not import {model}. Did you provide the correct path?') + if clazz not in base_classes: + base_classes += (clazz,) + return base_classes + + +class Target(*get_abstract_target_base_models()): pass From 0af247329565752ec7a1aba69e0d4a75a24fc0f2 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 5 Mar 2024 14:15:09 -0800 Subject: [PATCH 03/69] minor unrelated changes --- docs/brokers/create_broker.rst | 2 +- docs/observing/observation_module.rst | 2 +- tom_setup/templates/tom_setup/settings.tmpl | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/brokers/create_broker.rst b/docs/brokers/create_broker.rst index a4640ab42..e5f70c170 100644 --- a/docs/brokers/create_broker.rst +++ b/docs/brokers/create_broker.rst @@ -47,7 +47,7 @@ a Django project directory of the form: Creating a Broker Module ************************ -In this example, we will create a broker named __MyBroker__. +In this example, we will create a broker named **MyBroker**. Begin by creating a file ``my_broker.py``, and placing it in the inner ``mytom/`` directory of the project (in the directory with settings.py). ``my_broker.py`` will contain the classes that define our custom diff --git a/docs/observing/observation_module.rst b/docs/observing/observation_module.rst index 5b2db6f2b..2f4645f83 100644 --- a/docs/observing/observation_module.rst +++ b/docs/observing/observation_module.rst @@ -92,7 +92,7 @@ class: 'tom_observations.facilities.lco.LCOFacility', 'tom_observations.facilities.gemini.GEMFacility', 'tom_observations.facilities.soar.SOARFacility', - 'tom_observations.facilities.lt.LTFacility' + 'tom_observations.facilities.lt.LTFacility', 'mytom.myfacility.MyObservationFacility' ] diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index d2d123881..c9ed1d86d 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -243,12 +243,11 @@ TOM_FACILITY_CLASSES = [ TOM_ALERT_CLASSES = [ 'tom_alerts.brokers.alerce.ALeRCEBroker', - 'tom_alerts.brokers.antares.ANTARESBroker', + # 'tom_alerts.brokers.antares.ANTARESBroker', 'tom_alerts.brokers.gaia.GaiaBroker', 'tom_alerts.brokers.lasair.LasairBroker', - 'tom_alerts.brokers.scout.ScoutBroker', 'tom_alerts.brokers.tns.TNSBroker', - 'tom_alerts.brokers.fink.FinkBroker', + # 'tom_alerts.brokers.fink.FinkBroker', ] BROKERS = { From 4f304d3db504ba51be7db5d5fa6989c072f45082 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 5 Mar 2024 17:24:16 -0800 Subject: [PATCH 04/69] add default templates for user models to tom_setup --- tom_setup/management/commands/tom_setup.py | 22 +++++++++++++++++++-- tom_setup/templates/tom_setup/models.tmpl | 8 ++++++++ tom_setup/templates/tom_setup/settings.tmpl | 4 ++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 tom_setup/templates/tom_setup/models.tmpl diff --git a/tom_setup/management/commands/tom_setup.py b/tom_setup/management/commands/tom_setup.py index 18d90e75a..884cc7e4f 100644 --- a/tom_setup/management/commands/tom_setup.py +++ b/tom_setup/management/commands/tom_setup.py @@ -61,6 +61,11 @@ def create_project_dirs(self): ├── templates ├── tmp ├── mytom + │ ├── __init__.py + │ ├── models.py + │ ├── settings.py + │ ├── urls.py + │ └── wsgi.py └── static ├── .keep └── tom_common @@ -178,7 +183,8 @@ def generate_config(self): rendered = template.render(self.context) # TODO: Ugly hack to get project name - settings_location = os.path.join(BASE_DIR, os.path.basename(BASE_DIR), 'settings.py') + project_dir = os.path.join(BASE_DIR, os.path.basename(BASE_DIR)) + settings_location = os.path.join(project_dir, 'settings.py') if not os.path.exists(settings_location): msg = f'Could not determine settings.py location. Writing settings.py out to {settings_location}. ' \ f'Please copy file to the proper location after script finishes.' @@ -193,13 +199,24 @@ def generate_css(self): template = get_template('tom_setup/css.tmpl') rendered = template.render(self.context) - # TODO: Ugly hack to get project name css_location = os.path.join(BASE_DIR, 'static', 'tom_common', 'css', 'custom.css') with open(css_location, 'w+') as css_file: css_file.write(rendered) self.ok() + def generate_models(self): + self.status('Generating models.py... ') + template = get_template('tom_setup/models.tmpl') + rendered = template.render(self.context) + + # TODO: Ugly hack to get project name + models_location = os.path.join(BASE_DIR, os.path.basename(BASE_DIR), 'models.py') + with open(models_location, 'w+') as models_file: + models_file.write(rendered) + + self.ok() + def generate_urls(self): self.status('Generating urls.py... ') template = get_template('tom_setup/urls.tmpl') @@ -242,6 +259,7 @@ def handle(self, *args, **options): self.get_target_type() self.get_hint_preference() self.generate_config() + self.generate_models() self.generate_css() self.generate_urls() self.run_migrations() diff --git a/tom_setup/templates/tom_setup/models.tmpl b/tom_setup/templates/tom_setup/models.tmpl new file mode 100644 index 000000000..87c3fa5d9 --- /dev/null +++ b/tom_setup/templates/tom_setup/models.tmpl @@ -0,0 +1,8 @@ +from django.db import models + + +class UserDefinedTarget(models.Model): + pass + + class Meta: + abstract = True diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index c9ed1d86d..c81f13ed9 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -235,6 +235,10 @@ DATA_PROCESSORS = { 'spectroscopy': 'tom_dataproducts.processors.spectroscopy_processor.SpectroscopyProcessor', } +BASE_TARGET_MODELS = [ + '{{ PROJECT_NAME }}.models.UserDefinedTarget', +] + TOM_FACILITY_CLASSES = [ 'tom_observations.facilities.lco.LCOFacility', 'tom_observations.facilities.gemini.GEMFacility', From 65db577cf568cf71876dadba092b08aff1b14681 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 6 Mar 2024 16:24:22 -0800 Subject: [PATCH 05/69] add extended target data to target_detail page --- tom_targets/models.py | 15 +++++++++--- .../tom_targets/partials/target_data.html | 24 +++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index 9b0d695f5..fa7e11ca5 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -16,6 +16,8 @@ GLOBAL_TARGET_FIELDS = ['name', 'type'] +IGNORE_FIELDS = ['id', 'created', 'modified', 'aliases'] + SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ 'ra', 'dec', 'epoch', 'pm_ra', 'pm_dec', 'galactic_lng', 'galactic_lat', 'distance', 'distance_err' ] @@ -397,12 +399,14 @@ def tags(self): def as_dict(self): """ - Returns dictionary representation of attributes, excluding all attributes not associated with the ``type`` of - this ``Target``. + Returns dictionary representation of attributes, sets the order of attributes associated with the ``type`` of + this ``Target`` and then includes any additional attributes that are not empty and have not been 'hidden'. + :returns: Dictionary of key/value pairs representing target attributes :rtype: dict """ + # Get the ordered list of fields for the type of target if self.type == self.SIDEREAL: fields_for_type = SIDEREAL_FIELDS elif self.type == self.NON_SIDEREAL: @@ -410,7 +414,12 @@ def as_dict(self): else: fields_for_type = GLOBAL_TARGET_FIELDS - return model_to_dict(self, fields=fields_for_type) + # Get a list of all additional fields that are not empty and not hidden for this target + other_fields = [field.name for field in self._meta.get_fields() + if getattr(self, field.name, None) + and field.name not in fields_for_type and getattr(field, 'hidden', False) is False] + + return model_to_dict(self, fields=fields_for_type + other_fields) def give_user_access(self, user): """ diff --git a/tom_targets/templates/tom_targets/partials/target_data.html b/tom_targets/templates/tom_targets/partials/target_data.html index a2fa107b3..7b7549db1 100644 --- a/tom_targets/templates/tom_targets/partials/target_data.html +++ b/tom_targets/templates/tom_targets/partials/target_data.html @@ -9,18 +9,18 @@
{{ target_name }}
{% endfor %} {% for key, value in target.as_dict.items %} - {% if value and key != 'name' %} -
{% verbose_name target key %}
-
{{ value|truncate_number }}
- {% endif %} - {% if key == 'ra' %} -
 
-
{{ value|deg_to_sexigesimal:"hms" }}
- {% endif%} - {% if key == 'dec' %} -
 
-
{{ value|deg_to_sexigesimal:"dms" }}
- {% endif%} + {% if value and key != 'name' %} +
{% verbose_name target key %}
+
{{ value|truncate_number }}
+ {% endif %} + {% if key == 'ra' %} +
 
+
{{ value|deg_to_sexigesimal:"hms" }}
+ {% endif%} + {% if key == 'dec' %} +
 
+
{{ value|deg_to_sexigesimal:"dms" }}
+ {% endif%} {% endfor %}
From fed4871d11dc01576e1fb99ba7d14abef301b468 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 6 Mar 2024 16:42:51 -0800 Subject: [PATCH 06/69] add extra fields to target creation forms. --- tom_targets/forms.py | 11 ++++++++--- tom_targets/models.py | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tom_targets/forms.py b/tom_targets/forms.py index a752ef5ce..09feb29ab 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -9,7 +9,7 @@ from tom_dataproducts.sharing import get_sharing_destination_options from .models import ( Target, TargetExtra, TargetName, TargetList, SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS, - REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME + REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, IGNORE_FIELDS ) @@ -113,7 +113,9 @@ def __init__(self, *args, **kwargs): self.fields[field].required = True class Meta(TargetForm.Meta): - fields = SIDEREAL_FIELDS + # Include all fields except non-sidereal fields + fields = [field.name for field in Target._meta.get_fields() + if field.name not in NON_SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] class NonSiderealTargetCreateForm(TargetForm): @@ -144,7 +146,10 @@ def clean(self): ) class Meta(TargetForm.Meta): - fields = NON_SIDEREAL_FIELDS + # Include all fields except sidereal fields + print() + fields = [field.name for field in Target._meta.get_fields() + if field.name not in SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] class TargetVisibilityForm(forms.Form): diff --git a/tom_targets/models.py b/tom_targets/models.py index fa7e11ca5..783bd676c 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -16,7 +16,8 @@ GLOBAL_TARGET_FIELDS = ['name', 'type'] -IGNORE_FIELDS = ['id', 'created', 'modified', 'aliases'] +IGNORE_FIELDS = ['id', 'created', 'modified', 'aliases', 'targetextra', 'targetlist', 'observationrecord', + 'dataproduct', 'reduceddatum'] SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ 'ra', 'dec', 'epoch', 'pm_ra', 'pm_dec', 'galactic_lng', 'galactic_lat', 'distance', 'distance_err' From 5db66f3a331989d83e0f37384aaa756df3afed77 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 6 Mar 2024 17:56:19 -0800 Subject: [PATCH 07/69] include global fields --- tom_targets/forms.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 09feb29ab..3cfd5def9 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -9,7 +9,7 @@ from tom_dataproducts.sharing import get_sharing_destination_options from .models import ( Target, TargetExtra, TargetName, TargetList, SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS, - REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, IGNORE_FIELDS + REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, IGNORE_FIELDS, GLOBAL_TARGET_FIELDS ) @@ -114,8 +114,8 @@ def __init__(self, *args, **kwargs): class Meta(TargetForm.Meta): # Include all fields except non-sidereal fields - fields = [field.name for field in Target._meta.get_fields() - if field.name not in NON_SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] + fields = GLOBAL_TARGET_FIELDS + [field.name for field in Target._meta.get_fields() + if field.name not in NON_SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] class NonSiderealTargetCreateForm(TargetForm): @@ -147,9 +147,8 @@ def clean(self): class Meta(TargetForm.Meta): # Include all fields except sidereal fields - print() - fields = [field.name for field in Target._meta.get_fields() - if field.name not in SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] + fields = GLOBAL_TARGET_FIELDS + [field.name for field in Target._meta.get_fields() + if field.name not in SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] class TargetVisibilityForm(forms.Form): From c471939b6df704daadaf07750bfdd5a335850777 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 7 Mar 2024 14:43:19 -0800 Subject: [PATCH 08/69] be sure not to include fields that should be ignored --- tom_targets/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index 783bd676c..bf59f01a5 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -417,8 +417,9 @@ def as_dict(self): # Get a list of all additional fields that are not empty and not hidden for this target other_fields = [field.name for field in self._meta.get_fields() - if getattr(self, field.name, None) - and field.name not in fields_for_type and getattr(field, 'hidden', False) is False] + if getattr(self, field.name, None) is not None + and field.name not in fields_for_type + IGNORE_FIELDS + and getattr(field, 'hidden', False) is False] return model_to_dict(self, fields=fields_for_type + other_fields) From d05eb9bf83b89860fc3e1b92dee276e0c46d92c3 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Mon, 25 Mar 2024 16:18:28 -0700 Subject: [PATCH 09/69] make contexts and templates explicit --- tom_targets/models.py | 28 +++++++++++----------------- tom_targets/views.py | 3 +++ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index bf59f01a5..c99ec9dd5 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -276,9 +276,6 @@ class BaseTarget(models.Model): except (ImportError, AttributeError): matches = TargetMatchManager() - class Meta: - abstract = True - @transaction.atomic def save(self, *args, **kwargs): """ @@ -434,25 +431,22 @@ def give_user_access(self, user): assign_perm('targets.delete_target', user, self) -def get_abstract_target_base_models(): - base_classes = (BaseTarget,) +def get_target_base_model(): + base_class = BaseTarget try: - BASE_TARGET_MODELS = settings.BASE_TARGET_MODELS + BASE_TARGET_MODEL = settings.BASE_TARGET_MODEL except AttributeError: - return base_classes + return base_class - for model in BASE_TARGET_MODELS: - try: - clazz = import_string(model) - except (ImportError, AttributeError): - raise ImportError(f'Could not import {model}. Did you provide the correct path?') - if clazz not in base_classes: - base_classes += (clazz,) - return base_classes + try: + clazz = import_string(BASE_TARGET_MODEL) + return clazz + except (ImportError, AttributeError): + raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') + return base_class -class Target(*get_abstract_target_base_models()): - pass +Target = get_target_base_model() class TargetName(models.Model): diff --git a/tom_targets/views.py b/tom_targets/views.py index 1c4229281..d25b11904 100644 --- a/tom_targets/views.py +++ b/tom_targets/views.py @@ -105,6 +105,7 @@ class TargetCreateView(LoginRequiredMixin, CreateView): View for creating a Target. Requires authentication. """ + template_name = 'tom_targets/target_form.html' model = Target fields = '__all__' @@ -413,6 +414,7 @@ class TargetDetailView(Raise403PermissionRequiredMixin, DetailView): """ View that handles the display of the target details. Requires authorization. """ + template_name = 'tom_targets/target_detail.html' permission_required = 'tom_targets.view_target' model = Target @@ -433,6 +435,7 @@ def get_context_data(self, *args, **kwargs): ) observation_template_form.fields['target'].widget = HiddenInput() context['observation_template_form'] = observation_template_form + context['target'] = self.object return context def get(self, request, *args, **kwargs): From c7f42357be5ee5574a3fcab6f398c7eda00e7abc Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 26 Mar 2024 08:54:49 -0700 Subject: [PATCH 10/69] make templates explicit in views --- tom_targets/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tom_targets/views.py b/tom_targets/views.py index d25b11904..ffb00ac9a 100644 --- a/tom_targets/views.py +++ b/tom_targets/views.py @@ -235,6 +235,7 @@ class TargetUpdateView(Raise403PermissionRequiredMixin, UpdateView): """ View that handles updating a target. Requires authorization. """ + template_name = 'tom_targets/target_form.html' permission_required = 'tom_targets.change_target' model = Target fields = '__all__' @@ -334,6 +335,7 @@ class TargetDeleteView(Raise403PermissionRequiredMixin, DeleteView): """ View for deleting a target. Requires authorization. """ + template_name = 'tom_targets/target_confirm_delete.html' permission_required = 'tom_targets.delete_target' success_url = reverse_lazy('targets:list') model = Target From 67029c2539a2ce2a6c18dc199d6c197b6e03c85a Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 28 Mar 2024 16:32:04 -0700 Subject: [PATCH 11/69] move base target --- tom_observations/facilities/ocs.py | 3 +- tom_targets/base_models.py | 438 +++++++++++++++++++++++++++++ tom_targets/filters.py | 3 +- tom_targets/forms.py | 8 +- tom_targets/models.py | 428 +--------------------------- 5 files changed, 454 insertions(+), 426 deletions(-) create mode 100644 tom_targets/base_models.py diff --git a/tom_observations/facilities/ocs.py b/tom_observations/facilities/ocs.py index 7f3a8c2a2..cbb7093cb 100644 --- a/tom_observations/facilities/ocs.py +++ b/tom_observations/facilities/ocs.py @@ -15,7 +15,8 @@ from tom_observations.cadence import CadenceForm from tom_observations.facility import BaseRoboticObservationFacility, BaseRoboticObservationForm, get_service_class from tom_observations.observation_template import GenericTemplateForm -from tom_targets.models import Target, REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME +from tom_targets.models import Target +from tom_targets.base_models import REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME logger = logging.getLogger(__name__) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py new file mode 100644 index 000000000..7f1716955 --- /dev/null +++ b/tom_targets/base_models.py @@ -0,0 +1,438 @@ +import logging + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models, transaction +from django.forms.models import model_to_dict +from django.urls import reverse +from django.utils.module_loading import import_string +from guardian.shortcuts import assign_perm + +from tom_common.hooks import run_hook + +logger = logging.getLogger(__name__) +GLOBAL_TARGET_FIELDS = ['name', 'type'] + +IGNORE_FIELDS = ['id', 'created', 'modified', 'aliases', 'targetextra', 'targetlist', 'observationrecord', + 'dataproduct', 'reduceddatum'] + +SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ + 'ra', 'dec', 'epoch', 'pm_ra', 'pm_dec', 'galactic_lng', 'galactic_lat', 'distance', 'distance_err' +] + +NON_SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ + 'scheme', 'mean_anomaly', 'arg_of_perihelion', 'lng_asc_node', 'inclination', 'mean_daily_motion', 'semimajor_axis', + 'eccentricity', 'epoch_of_elements', 'epoch_of_perihelion', 'ephemeris_period', 'ephemeris_period_err', + 'ephemeris_epoch', 'ephemeris_epoch_err', 'perihdist' +] + +REQUIRED_SIDEREAL_FIELDS = ['ra', 'dec'] +REQUIRED_NON_SIDEREAL_FIELDS = [ + 'scheme', 'epoch_of_elements', 'inclination', 'lng_asc_node', 'arg_of_perihelion', 'eccentricity', +] +# Additional non-sidereal fields that are required for specific orbital element +# schemes +REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME = { + 'MPC_COMET': ['perihdist', 'epoch_of_perihelion'], + 'MPC_MINOR_PLANET': ['mean_anomaly', 'semimajor_axis'], + 'JPL_MAJOR_PLANET': ['mean_daily_motion', 'mean_anomaly', 'semimajor_axis'] +} + + +class TargetMatchManager(models.Manager): + """ + Search for matches amongst Target names and Aliases + Return Queryset containing relevant TARGET matches. + NOTE: check_for_fuzzy_match looks for Target names and aliases ignoring capitalization, spaces, dashes, underscores, + and parentheses. Additional matching functions can be added. + """ + + def check_for_fuzzy_match(self, name): + """ + Check for case-insensitive names ignoring spaces, dashes, underscore, and parentheses. + :param name: The string against which target names and aliases will be matched. + :return: queryset containing matching Targets. Will return targets even when matched value is an alias. + """ + simple_name = self.make_simple_name(name) + matching_names = [] + for target in self.get_queryset().all().prefetch_related('aliases'): + for alias in target.names: + if self.make_simple_name(alias) == simple_name: + matching_names.append(target.name) + queryset = self.get_queryset().filter(name__in=matching_names) + return queryset + + def make_simple_name(self, name): + """Create a simplified name to be used for comparison in check_for_fuzzy_match.""" + return name.lower().replace(" ", "").replace("-", "").replace("_", "").replace("(", "").replace(")", "") + + +class BaseTarget(models.Model): + """ + Class representing a target in a TOM + + :param name: The name of this target e.g. Barnard\'s star. + :type name: str + + :param type: The type of this target. + :type type: str + + :param created: The time at which this target was created in the TOM database. + :type type: datetime + + :param modified: The time at which this target was changed in the TOM database. + :type type: + + :param ra: Right Ascension, in degrees. + :type ra: float + + :param dec: Declination, in degrees. + :type dec: float + + :param epoch: Julian Years. Max 2100. + :type epoch: float + + :param parallax: Parallax, in milliarcseconds. + :type parallax: float + + :param pm_ra: Proper Motion: RA. Milliarsec/year. + :type pm_ra: float + + :param pm_dec: Proper Motion: Dec. Milliarsec/year. + :type pm_dec: float + + :param galactic_lng: Galactic Longitude in degrees. + :type galactic_lng: float + + :param galactic_lat: Galactic Latitude in degrees. + :type galactic_lat: float + + :param distance: Parsecs. + :type distance: float + + :param distance_err: Parsecs. + :type distance_err: float + + :param scheme: Orbital Element Scheme + :type scheme: str + + :param epoch_of_elements: Epoch of elements in JD. + :type epoch_of_elements: float + + :param mean_anomaly: Angle in degrees. + :type mean_anomaly: float + + :param arg_of_perihelion: Argument of Perhihelion. J2000. Degrees. + :type arg_of_perihelion: float + + :param eccentricity: Eccentricity + :type eccentricity: float + + :param lng_asc_node: Longitude of Ascending Node. J2000. Degrees. + :type lng_asc_node: float + + :param inclination: Inclination to the ecliptic. J2000. Degrees. + :type inclination: float + + :param mean_daily_motion: Degrees per day. + :type mean_daily_motion: float + + :param semimajor_axis: Semimajor Axis in AU + :type semimajor_axis: float + + :param epoch_of_perihelion: Julian Date. + :type epoch_of_perihelion: float + + :param ephemeris_period: Ephemeris period in days + :type ephemeris_period: float + + :param ephemeris_period_err: Days + :type ephemeris_period_err: float + + :param ephemeris_epoch: Days + :type ephemeris_epoch: float + + :param ephemeris_epoch_err: Days + :type ephemeris_epoch_err: float + """ + + SIDEREAL = 'SIDEREAL' + NON_SIDEREAL = 'NON_SIDEREAL' + TARGET_TYPES = ((SIDEREAL, 'Sidereal'), (NON_SIDEREAL, 'Non-sidereal')) + + TARGET_SCHEMES = ( + ('MPC_MINOR_PLANET', 'MPC Minor Planet'), + ('MPC_COMET', 'MPC Comet'), + ('JPL_MAJOR_PLANET', 'JPL Major Planet') + ) + + name = models.CharField( + max_length=100, default='', verbose_name='Name', help_text='The name of this target e.g. Barnard\'s star.', + unique=True + ) + type = models.CharField( + max_length=100, choices=TARGET_TYPES, verbose_name='Target Type', help_text='The type of this target.' + ) + created = models.DateTimeField( + auto_now_add=True, verbose_name='Time Created', + help_text='The time which this target was created in the TOM database.' + ) + modified = models.DateTimeField( + auto_now=True, verbose_name='Last Modified', + help_text='The time which this target was changed in the TOM database.' + ) + ra = models.FloatField( + null=True, blank=True, verbose_name='Right Ascension', help_text='Right Ascension, in degrees.' + ) + dec = models.FloatField( + null=True, blank=True, verbose_name='Declination', help_text='Declination, in degrees.' + ) + epoch = models.FloatField( + null=True, blank=True, verbose_name='Epoch', help_text='Julian Years. Max 2100.' + ) + parallax = models.FloatField( + null=True, blank=True, verbose_name='Parallax', help_text='Parallax, in milliarcseconds.' + ) + pm_ra = models.FloatField( + null=True, blank=True, verbose_name='Proper Motion (RA)', help_text='Proper Motion: RA. Milliarsec/year.' + ) + pm_dec = models.FloatField( + null=True, blank=True, verbose_name='Proper Motion (Declination)', + help_text='Proper Motion: Dec. Milliarsec/year.' + ) + galactic_lng = models.FloatField( + null=True, blank=True, verbose_name='Galactic Longitude', help_text='Galactic Longitude in degrees.' + ) + galactic_lat = models.FloatField( + null=True, blank=True, verbose_name='Galactic Latitude', help_text='Galactic Latitude in degrees.' + ) + distance = models.FloatField( + null=True, blank=True, verbose_name='Distance', help_text='Parsecs.' + ) + distance_err = models.FloatField( + null=True, blank=True, verbose_name='Distance Error', help_text='Parsecs.' + ) + scheme = models.CharField( + max_length=50, choices=TARGET_SCHEMES, verbose_name='Orbital Element Scheme', default='', blank=True + ) + epoch_of_elements = models.FloatField( + null=True, blank=True, verbose_name='Epoch of Elements', help_text='Julian date.' + ) + mean_anomaly = models.FloatField( + null=True, blank=True, verbose_name='Mean Anomaly', help_text='Angle in degrees.' + ) + arg_of_perihelion = models.FloatField( + null=True, blank=True, verbose_name='Argument of Perihelion', + help_text='Argument of Perhihelion. J2000. Degrees.' + ) + eccentricity = models.FloatField( + null=True, blank=True, verbose_name='Eccentricity', help_text='Eccentricity' + ) + lng_asc_node = models.FloatField( + null=True, blank=True, verbose_name='Longitude of Ascending Node', + help_text='Longitude of Ascending Node. J2000. Degrees.' + ) + inclination = models.FloatField( + null=True, blank=True, verbose_name='Inclination to the ecliptic', + help_text='Inclination to the ecliptic. J2000. Degrees.' + ) + mean_daily_motion = models.FloatField( + null=True, blank=True, verbose_name='Mean Daily Motion', help_text='Degrees per day.' + ) + semimajor_axis = models.FloatField( + null=True, blank=True, verbose_name='Semimajor Axis', help_text='In AU' + ) + epoch_of_perihelion = models.FloatField( + null=True, blank=True, verbose_name='Epoch of Perihelion', help_text='Julian Date.' + ) + ephemeris_period = models.FloatField( + null=True, blank=True, verbose_name='Ephemeris Period', help_text='Days' + ) + ephemeris_period_err = models.FloatField( + null=True, blank=True, verbose_name='Ephemeris Period Error', help_text='Days' + ) + ephemeris_epoch = models.FloatField( + null=True, blank=True, verbose_name='Ephemeris Epoch', help_text='Days' + ) + ephemeris_epoch_err = models.FloatField( + null=True, blank=True, verbose_name='Ephemeris Epoch Error', help_text='Days' + ) + perihdist = models.FloatField( + null=True, blank=True, verbose_name='Perihelion Distance', help_text='AU' + ) + + objects = models.Manager() + try: + target_match_manager = settings.MATCH_MANAGERS.get('Target') + try: + manager = import_string(target_match_manager) + matches = manager() + except (ImportError, AttributeError): + logger.debug(f'Could not import a Target Match Manager from {target_match_manager}. Did you provide the' + f'correct path in settings.py?') + raise ImportError + except (ImportError, AttributeError): + matches = TargetMatchManager() + + class Meta: + permissions = ( + ('view_target', 'View Target'), + ('add_target', 'Add Target'), + ('change_target', 'Change Target'), + ('delete_target', 'Delete Target'), + ) + + @transaction.atomic + def save(self, *args, **kwargs): + """ + Saves Target model data to the database, including extra fields. After saving to the database, also runs the + hook ``target_post_save``. The hook run is the one specified in ``settings.py``. + + :Keyword Arguments: + * extras (`dict`): dictionary of key/value pairs representing target attributes + """ + extras = kwargs.pop('extras', {}) + names = kwargs.pop('names', []) + + created = False if self.id else True + + super().save(*args, **kwargs) + + if created: + for extra_field in settings.EXTRA_FIELDS: + if extra_field.get('default') is not None: + TargetExtra(target=self, key=extra_field['name'], value=extra_field.get('default')).save() + + for k, v in extras.items(): + target_extra, _ = TargetExtra.objects.get_or_create(target=self, key=k) + target_extra.value = v + target_extra.save() + + for name in names: + name, _ = TargetName.objects.get_or_create(target=self, name=name) + name.full_clean() + name.save() + + if not created: + run_hook('target_post_save', target=self, created=created) + + def validate_unique(self, *args, **kwargs): + """ + Ensures that Target.name and all aliases of the target are unique. + Called automatically when checking form.is_valid(). + Should call Target.full_clean() to validate before save. + """ + super().validate_unique(*args, **kwargs) + # Check DB for similar target/alias names. + + matches = self.__class__.matches.check_for_fuzzy_match(self.name) + for match in matches: + # Ignore the fact that this target's name matches itself. + if match.id != self.id: + raise ValidationError(f'Target with Name or alias similar to {self.name} already exists') + # Alias Check only necessary when updating target existing target. Reverse relationships require Primary Key. + # If nothing has changed for the Target, do not validate against existing aliases. + if self.pk and self.name != self.__class__.objects.get(pk=self.pk).name: + for alias in self.aliases.all(): + # Check for fuzzy matching + if self.__class__.matches.make_simple_name(alias.name) == self.__class__.matches.make_simple_name(self.name): + raise ValidationError('Target name and target aliases must be different') + + def __str__(self): + return str(self.name) + + def get_absolute_url(self): + return reverse('targets:detail', kwargs={'pk': self.id}) + + def featured_image(self): + """ + Gets the ``DataProduct`` associated with this ``Target`` that is a FITS file and is uniquely marked as + "featured". + + :returns: ``DataProduct`` with data_product_type of ``fits_file`` and featured as ``True`` + :rtype: DataProduct + """ + return self.dataproduct_set.filter(data_product_type='fits_file', featured=True).first() + + @property + def names(self): + """ + Gets a list with the name and aliases of this target + + :returns: list of all names and `TargetName` values associated with this target + :rtype: list + """ + return [self.name] + [alias.name for alias in self.aliases.all()] + + @property + def future_observations(self): + """ + Gets all observations scheduled for this ``Target`` + + :returns: List of ``ObservationRecord`` objects without a terminal status + :rtype: list + """ + return [ + obs for obs in self.observationrecord_set.exclude(status='').order_by('scheduled_start') if not obs.terminal + ] + + @property + def extra_fields(self): + """ + Gets all ``TargetExtra`` fields associated with this ``Target``, provided the key is defined in ``settings.py`` + ``EXTRA_FIELDS`` + + :returns: Dictionary of key/value pairs representing target attributes + :rtype: dict + """ + defined_extras = [extra_field['name'] for extra_field in settings.EXTRA_FIELDS] + types = {extra_field['name']: extra_field['type'] for extra_field in settings.EXTRA_FIELDS} + return {te.key: te.typed_value(types[te.key]) + for te in self.targetextra_set.filter(key__in=defined_extras)} + + @property + def tags(self): + """ + Gets all ``TargetExtra`` fields associated with this ``Target``, provided the key is `NOT` defined in + ``settings.py`` ``EXTRA_FIELDS`` + + :returns: Dictionary of key/value pairs representing target attributes + :rtype: dict + """ + defined_extras = [extra_field['name'] for extra_field in settings.EXTRA_FIELDS] + return {te.key: te.value for te in self.targetextra_set.exclude(key__in=defined_extras)} + + def as_dict(self): + """ + Returns dictionary representation of attributes, sets the order of attributes associated with the ``type`` of + this ``Target`` and then includes any additional attributes that are not empty and have not been 'hidden'. + + + :returns: Dictionary of key/value pairs representing target attributes + :rtype: dict + """ + # Get the ordered list of fields for the type of target + if self.type == self.SIDEREAL: + fields_for_type = SIDEREAL_FIELDS + elif self.type == self.NON_SIDEREAL: + fields_for_type = NON_SIDEREAL_FIELDS + else: + fields_for_type = GLOBAL_TARGET_FIELDS + + # Get a list of all additional fields that are not empty and not hidden for this target + other_fields = [field.name for field in self._meta.get_fields() + if getattr(self, field.name, None) is not None + and field.name not in fields_for_type + IGNORE_FIELDS + and getattr(field, 'hidden', False) is False] + + return model_to_dict(self, fields=fields_for_type + other_fields) + + def give_user_access(self, user): + """ + Gives the given user permissions to view this target. + :param user: + :return: + """ + assign_perm('tom_targets.view_target', user, self) + assign_perm('tom_targets.change_target', user, self) + assign_perm('tom_targets.delete_target', user, self) diff --git a/tom_targets/filters.py b/tom_targets/filters.py index c47e7bd47..4afcb7523 100644 --- a/tom_targets/filters.py +++ b/tom_targets/filters.py @@ -2,7 +2,8 @@ from django.db.models import Q import django_filters -from tom_targets.models import Target, TargetList, TargetMatchManager +from tom_targets.models import Target, TargetList +from tom_targets.base_models import TargetMatchManager from tom_targets.utils import cone_search_filter diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 3cfd5def9..6ea06d448 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -7,10 +7,10 @@ from guardian.shortcuts import assign_perm, get_groups_with_perms, remove_perm from tom_dataproducts.sharing import get_sharing_destination_options -from .models import ( - Target, TargetExtra, TargetName, TargetList, SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS, - REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, IGNORE_FIELDS, GLOBAL_TARGET_FIELDS -) +from .models import Target, TargetExtra, TargetName, TargetList +from tom_targets.base_models import (SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS, + REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, + IGNORE_FIELDS, GLOBAL_TARGET_FIELDS) def extra_field_to_form_field(field_type): diff --git a/tom_targets/models.py b/tom_targets/models.py index c99ec9dd5..724eb3cf9 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -11,424 +11,11 @@ from guardian.shortcuts import assign_perm from tom_common.hooks import run_hook +from tom_targets.base_models import BaseTarget logger = logging.getLogger(__name__) -GLOBAL_TARGET_FIELDS = ['name', 'type'] -IGNORE_FIELDS = ['id', 'created', 'modified', 'aliases', 'targetextra', 'targetlist', 'observationrecord', - 'dataproduct', 'reduceddatum'] - -SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ - 'ra', 'dec', 'epoch', 'pm_ra', 'pm_dec', 'galactic_lng', 'galactic_lat', 'distance', 'distance_err' -] - -NON_SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ - 'scheme', 'mean_anomaly', 'arg_of_perihelion', 'lng_asc_node', 'inclination', 'mean_daily_motion', 'semimajor_axis', - 'eccentricity', 'epoch_of_elements', 'epoch_of_perihelion', 'ephemeris_period', 'ephemeris_period_err', - 'ephemeris_epoch', 'ephemeris_epoch_err', 'perihdist' -] - -REQUIRED_SIDEREAL_FIELDS = ['ra', 'dec'] -REQUIRED_NON_SIDEREAL_FIELDS = [ - 'scheme', 'epoch_of_elements', 'inclination', 'lng_asc_node', 'arg_of_perihelion', 'eccentricity', -] -# Additional non-sidereal fields that are required for specific orbital element -# schemes -REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME = { - 'MPC_COMET': ['perihdist', 'epoch_of_perihelion'], - 'MPC_MINOR_PLANET': ['mean_anomaly', 'semimajor_axis'], - 'JPL_MAJOR_PLANET': ['mean_daily_motion', 'mean_anomaly', 'semimajor_axis'] -} - - -class TargetMatchManager(models.Manager): - """ - Search for matches amongst Target names and Aliases - Return Queryset containing relevant TARGET matches. - NOTE: check_for_fuzzy_match looks for Target names and aliases ignoring capitalization, spaces, dashes, underscores, - and parentheses. Additional matching functions can be added. - """ - def check_for_fuzzy_match(self, name): - """ - Check for case-insensitive names ignoring spaces, dashes, underscore, and parentheses. - :param name: The string against which target names and aliases will be matched. - :return: queryset containing matching Targets. Will return targets even when matched value is an alias. - """ - simple_name = self.make_simple_name(name) - matching_names = [] - for target in Target.objects.all().prefetch_related('aliases'): - for alias in target.names: - if self.make_simple_name(alias) == simple_name: - matching_names.append(target.name) - queryset = Target.objects.filter(name__in=matching_names) - return queryset - - def make_simple_name(self, name): - """Create a simplified name to be used for comparison in check_for_fuzzy_match.""" - return name.lower().replace(" ", "").replace("-", "").replace("_", "").replace("(", "").replace(")", "") - - -class BaseTarget(models.Model): - """ - Class representing a target in a TOM - - :param name: The name of this target e.g. Barnard\'s star. - :type name: str - - :param type: The type of this target. - :type type: str - - :param created: The time at which this target was created in the TOM database. - :type type: datetime - - :param modified: The time at which this target was changed in the TOM database. - :type type: - - :param ra: Right Ascension, in degrees. - :type ra: float - - :param dec: Declination, in degrees. - :type dec: float - - :param epoch: Julian Years. Max 2100. - :type epoch: float - - :param parallax: Parallax, in milliarcseconds. - :type parallax: float - - :param pm_ra: Proper Motion: RA. Milliarsec/year. - :type pm_ra: float - - :param pm_dec: Proper Motion: Dec. Milliarsec/year. - :type pm_dec: float - - :param galactic_lng: Galactic Longitude in degrees. - :type galactic_lng: float - - :param galactic_lat: Galactic Latitude in degrees. - :type galactic_lat: float - - :param distance: Parsecs. - :type distance: float - - :param distance_err: Parsecs. - :type distance_err: float - - :param scheme: Orbital Element Scheme - :type scheme: str - - :param epoch_of_elements: Epoch of elements in JD. - :type epoch_of_elements: float - - :param mean_anomaly: Angle in degrees. - :type mean_anomaly: float - - :param arg_of_perihelion: Argument of Perhihelion. J2000. Degrees. - :type arg_of_perihelion: float - - :param eccentricity: Eccentricity - :type eccentricity: float - - :param lng_asc_node: Longitude of Ascending Node. J2000. Degrees. - :type lng_asc_node: float - - :param inclination: Inclination to the ecliptic. J2000. Degrees. - :type inclination: float - - :param mean_daily_motion: Degrees per day. - :type mean_daily_motion: float - - :param semimajor_axis: Semimajor Axis in AU - :type semimajor_axis: float - - :param epoch_of_perihelion: Julian Date. - :type epoch_of_perihelion: float - - :param ephemeris_period: Ephemeris period in days - :type ephemeris_period: float - - :param ephemeris_period_err: Days - :type ephemeris_period_err: float - - :param ephemeris_epoch: Days - :type ephemeris_epoch: float - - :param ephemeris_epoch_err: Days - :type ephemeris_epoch_err: float - """ - - SIDEREAL = 'SIDEREAL' - NON_SIDEREAL = 'NON_SIDEREAL' - TARGET_TYPES = ((SIDEREAL, 'Sidereal'), (NON_SIDEREAL, 'Non-sidereal')) - - TARGET_SCHEMES = ( - ('MPC_MINOR_PLANET', 'MPC Minor Planet'), - ('MPC_COMET', 'MPC Comet'), - ('JPL_MAJOR_PLANET', 'JPL Major Planet') - ) - - name = models.CharField( - max_length=100, default='', verbose_name='Name', help_text='The name of this target e.g. Barnard\'s star.', - unique=True - ) - type = models.CharField( - max_length=100, choices=TARGET_TYPES, verbose_name='Target Type', help_text='The type of this target.' - ) - created = models.DateTimeField( - auto_now_add=True, verbose_name='Time Created', - help_text='The time which this target was created in the TOM database.' - ) - modified = models.DateTimeField( - auto_now=True, verbose_name='Last Modified', - help_text='The time which this target was changed in the TOM database.' - ) - ra = models.FloatField( - null=True, blank=True, verbose_name='Right Ascension', help_text='Right Ascension, in degrees.' - ) - dec = models.FloatField( - null=True, blank=True, verbose_name='Declination', help_text='Declination, in degrees.' - ) - epoch = models.FloatField( - null=True, blank=True, verbose_name='Epoch', help_text='Julian Years. Max 2100.' - ) - parallax = models.FloatField( - null=True, blank=True, verbose_name='Parallax', help_text='Parallax, in milliarcseconds.' - ) - pm_ra = models.FloatField( - null=True, blank=True, verbose_name='Proper Motion (RA)', help_text='Proper Motion: RA. Milliarsec/year.' - ) - pm_dec = models.FloatField( - null=True, blank=True, verbose_name='Proper Motion (Declination)', - help_text='Proper Motion: Dec. Milliarsec/year.' - ) - galactic_lng = models.FloatField( - null=True, blank=True, verbose_name='Galactic Longitude', help_text='Galactic Longitude in degrees.' - ) - galactic_lat = models.FloatField( - null=True, blank=True, verbose_name='Galactic Latitude', help_text='Galactic Latitude in degrees.' - ) - distance = models.FloatField( - null=True, blank=True, verbose_name='Distance', help_text='Parsecs.' - ) - distance_err = models.FloatField( - null=True, blank=True, verbose_name='Distance Error', help_text='Parsecs.' - ) - scheme = models.CharField( - max_length=50, choices=TARGET_SCHEMES, verbose_name='Orbital Element Scheme', default='', blank=True - ) - epoch_of_elements = models.FloatField( - null=True, blank=True, verbose_name='Epoch of Elements', help_text='Julian date.' - ) - mean_anomaly = models.FloatField( - null=True, blank=True, verbose_name='Mean Anomaly', help_text='Angle in degrees.' - ) - arg_of_perihelion = models.FloatField( - null=True, blank=True, verbose_name='Argument of Perihelion', - help_text='Argument of Perhihelion. J2000. Degrees.' - ) - eccentricity = models.FloatField( - null=True, blank=True, verbose_name='Eccentricity', help_text='Eccentricity' - ) - lng_asc_node = models.FloatField( - null=True, blank=True, verbose_name='Longitude of Ascending Node', - help_text='Longitude of Ascending Node. J2000. Degrees.' - ) - inclination = models.FloatField( - null=True, blank=True, verbose_name='Inclination to the ecliptic', - help_text='Inclination to the ecliptic. J2000. Degrees.' - ) - mean_daily_motion = models.FloatField( - null=True, blank=True, verbose_name='Mean Daily Motion', help_text='Degrees per day.' - ) - semimajor_axis = models.FloatField( - null=True, blank=True, verbose_name='Semimajor Axis', help_text='In AU' - ) - epoch_of_perihelion = models.FloatField( - null=True, blank=True, verbose_name='Epoch of Perihelion', help_text='Julian Date.' - ) - ephemeris_period = models.FloatField( - null=True, blank=True, verbose_name='Ephemeris Period', help_text='Days' - ) - ephemeris_period_err = models.FloatField( - null=True, blank=True, verbose_name='Ephemeris Period Error', help_text='Days' - ) - ephemeris_epoch = models.FloatField( - null=True, blank=True, verbose_name='Ephemeris Epoch', help_text='Days' - ) - ephemeris_epoch_err = models.FloatField( - null=True, blank=True, verbose_name='Ephemeris Epoch Error', help_text='Days' - ) - perihdist = models.FloatField( - null=True, blank=True, verbose_name='Perihelion Distance', help_text='AU' - ) - - objects = models.Manager() - try: - target_match_manager = settings.MATCH_MANAGERS.get('Target') - try: - manager = import_string(target_match_manager) - matches = manager() - except (ImportError, AttributeError): - logger.debug(f'Could not import a Target Match Manager from {target_match_manager}. Did you provide the' - f'correct path in settings.py?') - raise ImportError - except (ImportError, AttributeError): - matches = TargetMatchManager() - - @transaction.atomic - def save(self, *args, **kwargs): - """ - Saves Target model data to the database, including extra fields. After saving to the database, also runs the - hook ``target_post_save``. The hook run is the one specified in ``settings.py``. - - :Keyword Arguments: - * extras (`dict`): dictionary of key/value pairs representing target attributes - """ - extras = kwargs.pop('extras', {}) - names = kwargs.pop('names', []) - - created = False if self.id else True - - super().save(*args, **kwargs) - - if created: - for extra_field in settings.EXTRA_FIELDS: - if extra_field.get('default') is not None: - TargetExtra(target=self, key=extra_field['name'], value=extra_field.get('default')).save() - - for k, v in extras.items(): - target_extra, _ = TargetExtra.objects.get_or_create(target=self, key=k) - target_extra.value = v - target_extra.save() - - for name in names: - name, _ = TargetName.objects.get_or_create(target=self, name=name) - name.full_clean() - name.save() - - if not created: - run_hook('target_post_save', target=self, created=created) - - def validate_unique(self, *args, **kwargs): - """ - Ensures that Target.name and all aliases of the target are unique. - Called automatically when checking form.is_valid(). - Should call Target.full_clean() to validate before save. - """ - super().validate_unique(*args, **kwargs) - # Check DB for similar target/alias names. - matches = Target.matches.check_for_fuzzy_match(self.name) - for match in matches: - # Ignore the fact that this target's name matches itself. - if match.id != self.id: - raise ValidationError(f'Target with Name or alias similar to {self.name} already exists') - # Alias Check only necessary when updating target existing target. Reverse relationships require Primary Key. - # If nothing has changed for the Target, do not validate against existing aliases. - if self.pk and self.name != Target.objects.get(pk=self.pk).name: - for alias in self.aliases.all(): - # Check for fuzzy matching - if Target.matches.make_simple_name(alias.name) == Target.matches.make_simple_name(self.name): - raise ValidationError('Target name and target aliases must be different') - - def __str__(self): - return str(self.name) - - def get_absolute_url(self): - return reverse('targets:detail', kwargs={'pk': self.id}) - - def featured_image(self): - """ - Gets the ``DataProduct`` associated with this ``Target`` that is a FITS file and is uniquely marked as - "featured". - - :returns: ``DataProduct`` with data_product_type of ``fits_file`` and featured as ``True`` - :rtype: DataProduct - """ - return self.dataproduct_set.filter(data_product_type='fits_file', featured=True).first() - - @property - def names(self): - """ - Gets a list with the name and aliases of this target - - :returns: list of all names and `TargetName` values associated with this target - :rtype: list - """ - return [self.name] + [alias.name for alias in self.aliases.all()] - - @property - def future_observations(self): - """ - Gets all observations scheduled for this ``Target`` - - :returns: List of ``ObservationRecord`` objects without a terminal status - :rtype: list - """ - return [ - obs for obs in self.observationrecord_set.exclude(status='').order_by('scheduled_start') if not obs.terminal - ] - - @property - def extra_fields(self): - """ - Gets all ``TargetExtra`` fields associated with this ``Target``, provided the key is defined in ``settings.py`` - ``EXTRA_FIELDS`` - - :returns: Dictionary of key/value pairs representing target attributes - :rtype: dict - """ - defined_extras = [extra_field['name'] for extra_field in settings.EXTRA_FIELDS] - types = {extra_field['name']: extra_field['type'] for extra_field in settings.EXTRA_FIELDS} - return {te.key: te.typed_value(types[te.key]) - for te in self.targetextra_set.filter(key__in=defined_extras)} - - @property - def tags(self): - """ - Gets all ``TargetExtra`` fields associated with this ``Target``, provided the key is `NOT` defined in - ``settings.py`` ``EXTRA_FIELDS`` - - :returns: Dictionary of key/value pairs representing target attributes - :rtype: dict - """ - defined_extras = [extra_field['name'] for extra_field in settings.EXTRA_FIELDS] - return {te.key: te.value for te in self.targetextra_set.exclude(key__in=defined_extras)} - - def as_dict(self): - """ - Returns dictionary representation of attributes, sets the order of attributes associated with the ``type`` of - this ``Target`` and then includes any additional attributes that are not empty and have not been 'hidden'. - - - :returns: Dictionary of key/value pairs representing target attributes - :rtype: dict - """ - # Get the ordered list of fields for the type of target - if self.type == self.SIDEREAL: - fields_for_type = SIDEREAL_FIELDS - elif self.type == self.NON_SIDEREAL: - fields_for_type = NON_SIDEREAL_FIELDS - else: - fields_for_type = GLOBAL_TARGET_FIELDS - - # Get a list of all additional fields that are not empty and not hidden for this target - other_fields = [field.name for field in self._meta.get_fields() - if getattr(self, field.name, None) is not None - and field.name not in fields_for_type + IGNORE_FIELDS - and getattr(field, 'hidden', False) is False] - - return model_to_dict(self, fields=fields_for_type + other_fields) - - def give_user_access(self, user): - """ - Gives the given user permissions to view this target. - :param user: - :return: - """ - assign_perm('targets.view_target', user, self) - assign_perm('targets.change_target', user, self) - assign_perm('targets.delete_target', user, self) def get_target_base_model(): @@ -438,12 +25,13 @@ def get_target_base_model(): except AttributeError: return base_class - try: - clazz = import_string(BASE_TARGET_MODEL) - return clazz - except (ImportError, AttributeError): - raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') - return base_class + # try: + clazz = import_string('mycode.models.UserDefinedTarget') + return clazz + # except (ImportError): + # raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') + # return base_class + Target = get_target_base_model() From baa505f3ed72b03cffb26831548ffeb4acec3d67 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 2 Apr 2024 11:04:59 -0700 Subject: [PATCH 12/69] use relatedobject managers --- tom_targets/base_models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 7f1716955..7ecd4da1a 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -301,15 +301,15 @@ def save(self, *args, **kwargs): if created: for extra_field in settings.EXTRA_FIELDS: if extra_field.get('default') is not None: - TargetExtra(target=self, key=extra_field['name'], value=extra_field.get('default')).save() + self.targetextra_set(target=self, key=extra_field['name'], value=extra_field.get('default')).save() for k, v in extras.items(): - target_extra, _ = TargetExtra.objects.get_or_create(target=self, key=k) + target_extra, _ = self.targetextra_set.get_or_create(target=self, key=k) target_extra.value = v target_extra.save() for name in names: - name, _ = TargetName.objects.get_or_create(target=self, name=name) + name, _ = self.targetname_set.get_or_create(target=self, name=name) name.full_clean() name.save() From 57ffa2fc8b99f46fb067e96b4896c1d96b1479ee Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 2 Apr 2024 16:36:09 -0700 Subject: [PATCH 13/69] set BaseTarget verbose name --- tom_targets/base_models.py | 1 + tom_targets/models.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 7ecd4da1a..07df08d53 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -275,6 +275,7 @@ class BaseTarget(models.Model): matches = TargetMatchManager() class Meta: + verbose_name = "target" permissions = ( ('view_target', 'View Target'), ('add_target', 'Add Target'), diff --git a/tom_targets/models.py b/tom_targets/models.py index 724eb3cf9..c38a229b3 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -20,14 +20,14 @@ def get_target_base_model(): base_class = BaseTarget - try: - BASE_TARGET_MODEL = settings.BASE_TARGET_MODEL - except AttributeError: - return base_class + # try: + # BASE_TARGET_MODEL = settings.BASE_TARGET_MODEL + # except AttributeError: + return base_class # try: - clazz = import_string('mycode.models.UserDefinedTarget') - return clazz + # clazz = import_string('mycode.models.UserDefinedTarget') + # return clazz # except (ImportError): # raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') # return base_class From ada27695f760c344c1be522c387c8ff2f5722c1c Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 3 Apr 2024 15:21:28 -0700 Subject: [PATCH 14/69] point relational fields directly at Basetarget --- tom_dataproducts/models.py | 5 +++-- tom_observations/models.py | 4 ++-- tom_targets/forms.py | 8 ++------ tom_targets/models.py | 28 +++++++++++----------------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/tom_dataproducts/models.py b/tom_dataproducts/models.py index baad56ca9..903d66aab 100644 --- a/tom_dataproducts/models.py +++ b/tom_dataproducts/models.py @@ -13,6 +13,7 @@ from importlib import import_module from tom_targets.models import Target +from tom_targets.base_models import BaseTarget from tom_alerts.models import AlertStreamMessage from tom_observations.models import ObservationRecord @@ -188,7 +189,7 @@ class DataProduct(models.Model): null=True, help_text='Data product identifier used by the source of the data product.' ) - target = models.ForeignKey(Target, on_delete=models.CASCADE) + target = models.ForeignKey(BaseTarget, on_delete=models.CASCADE) observation_record = models.ForeignKey(ObservationRecord, null=True, default=None, on_delete=models.CASCADE) data = models.FileField(upload_to=data_product_path, null=True, default=None) extra_data = models.TextField(blank=True, default='') @@ -352,7 +353,7 @@ class ReducedDatum(models.Model): """ - target = models.ForeignKey(Target, null=False, on_delete=models.CASCADE) + target = models.ForeignKey(BaseTarget, null=False, on_delete=models.CASCADE) data_product = models.ForeignKey(DataProduct, null=True, blank=True, on_delete=models.CASCADE) data_type = models.CharField( max_length=100, diff --git a/tom_observations/models.py b/tom_observations/models.py index d44af2423..88f97ea21 100644 --- a/tom_observations/models.py +++ b/tom_observations/models.py @@ -1,9 +1,9 @@ from django.contrib.auth.models import User from django.db import models -from tom_targets.models import Target from tom_observations.facility import get_service_class from tom_common.hooks import run_hook +from tom_targets.base_models import BaseTarget class ObservationRecord(models.Model): @@ -39,7 +39,7 @@ class ObservationRecord(models.Model): :param modified: The time at which this object was last updated. :type modified: datetime """ - target = models.ForeignKey(Target, on_delete=models.CASCADE) + target = models.ForeignKey(BaseTarget, on_delete=models.CASCADE) user = models.ForeignKey(User, null=True, default=None, on_delete=models.DO_NOTHING) facility = models.CharField(max_length=50) parameters = models.JSONField() diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 6ea06d448..73a9f975f 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -113,9 +113,7 @@ def __init__(self, *args, **kwargs): self.fields[field].required = True class Meta(TargetForm.Meta): - # Include all fields except non-sidereal fields - fields = GLOBAL_TARGET_FIELDS + [field.name for field in Target._meta.get_fields() - if field.name not in NON_SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] + fields = SIDEREAL_FIELDS class NonSiderealTargetCreateForm(TargetForm): @@ -146,9 +144,7 @@ def clean(self): ) class Meta(TargetForm.Meta): - # Include all fields except sidereal fields - fields = GLOBAL_TARGET_FIELDS + [field.name for field in Target._meta.get_fields() - if field.name not in SIDEREAL_FIELDS and field.name not in IGNORE_FIELDS] + fields = NON_SIDEREAL_FIELDS class TargetVisibilityForm(forms.Form): diff --git a/tom_targets/models.py b/tom_targets/models.py index c38a229b3..edbf4a8e3 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -16,22 +16,16 @@ logger = logging.getLogger(__name__) - - def get_target_base_model(): base_class = BaseTarget - # try: - # BASE_TARGET_MODEL = settings.BASE_TARGET_MODEL - # except AttributeError: - return base_class - - # try: - # clazz = import_string('mycode.models.UserDefinedTarget') - # return clazz - # except (ImportError): - # raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') - # return base_class - + try: + BASE_TARGET_MODEL = settings.BASE_TARGET_MODEL + clazz = import_string(BASE_TARGET_MODEL) + return clazz + except AttributeError: + return base_class + except ImportError: + raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') Target = get_target_base_model() @@ -52,7 +46,7 @@ class TargetName(models.Model): :param modified: The time at which this target name was modified in the TOM database. :type modified: datetime """ - target = models.ForeignKey(Target, on_delete=models.CASCADE, related_name='aliases') + target = models.ForeignKey(BaseTarget, on_delete=models.CASCADE, related_name='aliases') name = models.CharField(max_length=100, unique=True, verbose_name='Alias') created = models.DateTimeField( auto_now_add=True, help_text='The time at which this target name was created.' @@ -110,7 +104,7 @@ class TargetExtra(models.Model): :param time_value: Datetime representation of the ``value`` field for this object, if applicable. :type time_value: datetime """ - target = models.ForeignKey(Target, on_delete=models.CASCADE) + target = models.ForeignKey(BaseTarget, on_delete=models.CASCADE) key = models.CharField(max_length=200) value = models.TextField(blank=True, default='') float_value = models.FloatField(null=True, blank=True) @@ -187,7 +181,7 @@ class TargetList(models.Model): :type modified: datetime """ name = models.CharField(max_length=200, help_text='The name of the target list.') - targets = models.ManyToManyField(Target) + targets = models.ManyToManyField(BaseTarget) created = models.DateTimeField( auto_now_add=True, help_text='The time which this target list was created in the TOM database.' ) From a190f835872a3013eb9158ce61b1be1a2dc7ab47 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 3 Apr 2024 15:26:21 -0700 Subject: [PATCH 15/69] remove unused imports --- tom_dataproducts/models.py | 3 +-- tom_targets/base_models.py | 3 ++- tom_targets/forms.py | 3 +-- tom_targets/models.py | 6 +----- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tom_dataproducts/models.py b/tom_dataproducts/models.py index 903d66aab..f01f245be 100644 --- a/tom_dataproducts/models.py +++ b/tom_dataproducts/models.py @@ -12,7 +12,6 @@ from PIL import Image from importlib import import_module -from tom_targets.models import Target from tom_targets.base_models import BaseTarget from tom_alerts.models import AlertStreamMessage from tom_observations.models import ObservationRecord @@ -294,7 +293,7 @@ def create_thumbnail(self, width=None, height=None): if resp: return tmpfile except Exception as e: - logger.warn(f'Unable to create thumbnail for {self}: {e}') + logger.warning(f'Unable to create thumbnail for {self}: {e}') return diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 07df08d53..a6a2834d3 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -336,7 +336,8 @@ def validate_unique(self, *args, **kwargs): if self.pk and self.name != self.__class__.objects.get(pk=self.pk).name: for alias in self.aliases.all(): # Check for fuzzy matching - if self.__class__.matches.make_simple_name(alias.name) == self.__class__.matches.make_simple_name(self.name): + if self.__class__.matches.make_simple_name(alias.name) == \ + self.__class__.matches.make_simple_name(self.name): raise ValidationError('Target name and target aliases must be different') def __str__(self): diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 73a9f975f..0ebfac3b7 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -9,8 +9,7 @@ from tom_dataproducts.sharing import get_sharing_destination_options from .models import Target, TargetExtra, TargetName, TargetList from tom_targets.base_models import (SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS, - REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, - IGNORE_FIELDS, GLOBAL_TARGET_FIELDS) + REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME) def extra_field_to_form_field(field_type): diff --git a/tom_targets/models.py b/tom_targets/models.py index edbf4a8e3..9fe4f7e18 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -4,13 +4,9 @@ from django.conf import settings from django.core.exceptions import ValidationError -from django.db import models, transaction -from django.forms.models import model_to_dict -from django.urls import reverse +from django.db import models from django.utils.module_loading import import_string -from guardian.shortcuts import assign_perm -from tom_common.hooks import run_hook from tom_targets.base_models import BaseTarget logger = logging.getLogger(__name__) From a11fee132b681a1d4502768200c16665791f152e Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 4 Apr 2024 16:18:24 -0700 Subject: [PATCH 16/69] set app permissions for django guardian --- tom_targets/base_models.py | 2 +- tom_targets/views.py | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index a6a2834d3..76e7e8ba3 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -14,7 +14,7 @@ GLOBAL_TARGET_FIELDS = ['name', 'type'] IGNORE_FIELDS = ['id', 'created', 'modified', 'aliases', 'targetextra', 'targetlist', 'observationrecord', - 'dataproduct', 'reduceddatum'] + 'dataproduct', 'reduceddatum', 'basetarget_ptr'] SIDEREAL_FIELDS = GLOBAL_TARGET_FIELDS + [ 'ra', 'dec', 'epoch', 'pm_ra', 'pm_dec', 'galactic_lng', 'galactic_lat', 'distance', 'distance_err' diff --git a/tom_targets/views.py b/tom_targets/views.py index ffb00ac9a..e53f72a22 100644 --- a/tom_targets/views.py +++ b/tom_targets/views.py @@ -59,7 +59,8 @@ class TargetListView(PermissionListMixin, FilterView): strict = False model = Target filterset_class = TargetFilter - permission_required = 'tom_targets.view_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.view_target' ordering = ['-created'] def get_context_data(self, *args, **kwargs): @@ -236,7 +237,8 @@ class TargetUpdateView(Raise403PermissionRequiredMixin, UpdateView): View that handles updating a target. Requires authorization. """ template_name = 'tom_targets/target_form.html' - permission_required = 'tom_targets.change_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.change_target' model = Target fields = '__all__' @@ -336,7 +338,8 @@ class TargetDeleteView(Raise403PermissionRequiredMixin, DeleteView): View for deleting a target. Requires authorization. """ template_name = 'tom_targets/target_confirm_delete.html' - permission_required = 'tom_targets.delete_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.delete_target' success_url = reverse_lazy('targets:list') model = Target @@ -346,7 +349,8 @@ class TargetShareView(FormView): View for sharing a target. Requires authorization. """ template_name = 'tom_targets/target_share.html' - permission_required = 'tom_targets.share_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.share_target' form_class = TargetShareForm def get_context_data(self, *args, **kwargs): @@ -417,7 +421,8 @@ class TargetDetailView(Raise403PermissionRequiredMixin, DetailView): View that handles the display of the target details. Requires authorization. """ template_name = 'tom_targets/target_detail.html' - permission_required = 'tom_targets.view_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.view_target' model = Target def get_context_data(self, *args, **kwargs): @@ -479,7 +484,8 @@ def get(self, request, *args, **kwargs): class TargetHermesPreloadView(SingleObjectMixin, View): model = Target - permission_required = 'tom_targets.share_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.share_target' def post(self, request, *args, **kwargs): target = self.get_object() @@ -658,7 +664,8 @@ class TargetGroupingShareView(FormView): View for sharing a TargetList. Requires authorization. """ template_name = 'tom_targets/target_group_share.html' - permission_required = 'tom_targets.share_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.share_target' form_class = TargetListShareForm def get_context_data(self, *args, **kwargs): @@ -727,7 +734,8 @@ def form_valid(self, form): class TargetGroupingHermesPreloadView(SingleObjectMixin, View): model = TargetList - permission_required = 'tom_targets.share_target' + # Set app_name for Django-Guardian Permissions in case of Custom Target Model + permission_required = f'{Target._meta.app_label}.share_target' def post(self, request, *args, **kwargs): targetlist = self.get_object() From e750fd03b8f1b90ff1b66ec52fa29731bcd63142 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 4 Apr 2024 17:34:12 -0700 Subject: [PATCH 17/69] add extra target fields to sidereal/non-sidereal creation forms --- tom_targets/forms.py | 9 ++++++--- tom_targets/models.py | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 0ebfac3b7..3b843112b 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -9,7 +9,8 @@ from tom_dataproducts.sharing import get_sharing_destination_options from .models import Target, TargetExtra, TargetName, TargetList from tom_targets.base_models import (SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS, - REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME) + REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME, + IGNORE_FIELDS) def extra_field_to_form_field(field_type): @@ -112,7 +113,8 @@ def __init__(self, *args, **kwargs): self.fields[field].required = True class Meta(TargetForm.Meta): - fields = SIDEREAL_FIELDS + fields = SIDEREAL_FIELDS + [field.name for field in Target._meta.get_fields() + if field.name not in SIDEREAL_FIELDS + IGNORE_FIELDS + NON_SIDEREAL_FIELDS] class NonSiderealTargetCreateForm(TargetForm): @@ -143,7 +145,8 @@ def clean(self): ) class Meta(TargetForm.Meta): - fields = NON_SIDEREAL_FIELDS + fields = NON_SIDEREAL_FIELDS + [field.name for field in Target._meta.get_fields() + if field.name not in SIDEREAL_FIELDS + IGNORE_FIELDS + NON_SIDEREAL_FIELDS] class TargetVisibilityForm(forms.Form): diff --git a/tom_targets/models.py b/tom_targets/models.py index 9fe4f7e18..4b92ea763 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -26,7 +26,6 @@ def get_target_base_model(): Target = get_target_base_model() - class TargetName(models.Model): """ Class representing an alternative name for a ``Target``. From 3552a5d45ed52df54f0f400e886d899d44e24295 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 5 Apr 2024 16:34:04 -0700 Subject: [PATCH 18/69] update tom_setup etc --- tom_setup/management/commands/tom_setup.py | 32 +++++++++++++++++++-- tom_setup/templates/tom_setup/models.tmpl | 13 +++++++-- tom_setup/templates/tom_setup/settings.tmpl | 4 +++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/tom_setup/management/commands/tom_setup.py b/tom_setup/management/commands/tom_setup.py index 884cc7e4f..fcf88e792 100644 --- a/tom_setup/management/commands/tom_setup.py +++ b/tom_setup/management/commands/tom_setup.py @@ -48,8 +48,27 @@ def check_python(self): self.status('Checking Python version... ') major = sys.version_info.major minor = sys.version_info.minor - if major < 3 or minor < 7: - self.exit('Incompatible Python version found. Please install Python >= 3.7') + if major < 3 or minor < 8: + self.exit('Incompatible Python version found. Please install Python >= 3.8') + self.ok() + + def create_custom_code_app(self): + custom_code_app_explanation = ('You will need an app to store your custom code. \n' + 'This app will be created in the same directory as your project and should have ' + 'a different name from your main project name. \n') + prompt = f'What would you like to name your custom code app? {self.style.WARNING("[custom_code] ")}' + self.stdout.write(custom_code_app_explanation) + while True: + response = input(prompt).lower().replace(' ', '_') + if response == os.path.basename(BASE_DIR).lower(): + self.stdout.write('Invalid response. Please try again.') + elif not response: + self.context['CUSTOM_CODE_APP_NAME'] = 'custom_code' + break + else: + self.context['CUSTOM_CODE_APP_NAME'] = response + break + call_command('startapp', response) self.ok() def create_project_dirs(self): @@ -66,6 +85,12 @@ def create_project_dirs(self): │ ├── settings.py │ ├── urls.py │ └── wsgi.py + ├── myapp + │ ├── __init__.py + │ ├── migrations + │ ├── admin.py + │ ├── apps.py + │ └── models.py └── static ├── .keep └── tom_common @@ -211,7 +236,7 @@ def generate_models(self): rendered = template.render(self.context) # TODO: Ugly hack to get project name - models_location = os.path.join(BASE_DIR, os.path.basename(BASE_DIR), 'models.py') + models_location = os.path.join(BASE_DIR, self.context['CUSTOM_CODE_APP_NAME'], 'models.py') with open(models_location, 'w+') as models_file: models_file.write(rendered) @@ -254,6 +279,7 @@ def handle(self, *args, **options): self.context['PROJECT_NAME'] = os.path.basename(BASE_DIR) self.welcome_banner() self.check_python() + self.create_custom_code_app() self.create_project_dirs() self.generate_secret_key() self.get_target_type() diff --git a/tom_setup/templates/tom_setup/models.tmpl b/tom_setup/templates/tom_setup/models.tmpl index 87c3fa5d9..2fc354e02 100644 --- a/tom_setup/templates/tom_setup/models.tmpl +++ b/tom_setup/templates/tom_setup/models.tmpl @@ -1,8 +1,15 @@ from django.db import models +from tom_targets.base_models import BaseTarget -class UserDefinedTarget(models.Model): - pass + +class UserDefinedTarget(BaseTarget): class Meta: - abstract = True + verbose_name = "target" + permissions = ( + ('view_target', 'View Target'), + ('add_target', 'Add Target'), + ('change_target', 'Change Target'), + ('delete_target', 'Delete Target'), + ) diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index c81f13ed9..efb5d0af5 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'tom_catalogs', 'tom_observations', 'tom_dataproducts', + '{{ CUSTOM_CODE_APP_NAME }}', ] SITE_ID = 1 @@ -193,6 +194,9 @@ CACHES = { # TOM Specific configuration TARGET_TYPE = '{{ TARGET_TYPE }}' +# Set to the full path of a custom target model to extend the BaseTarget Model with custom fields. +# BASE_TARGET_MODEL = '{{ CUSTOM_CODE_APP_NAME }}.models.UserDefinedTarget' + FACILITIES = { 'LCO': { 'portal_url': 'https://observe.lco.global', From 7ca9954873735cb6d444397c77bd797eda75456a Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 11 Apr 2024 16:24:29 -0700 Subject: [PATCH 19/69] allow for updating targets --- tom_setup/management/commands/tom_setup.py | 2 +- tom_targets/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tom_setup/management/commands/tom_setup.py b/tom_setup/management/commands/tom_setup.py index fcf88e792..f0b1098ca 100644 --- a/tom_setup/management/commands/tom_setup.py +++ b/tom_setup/management/commands/tom_setup.py @@ -59,7 +59,7 @@ def create_custom_code_app(self): prompt = f'What would you like to name your custom code app? {self.style.WARNING("[custom_code] ")}' self.stdout.write(custom_code_app_explanation) while True: - response = input(prompt).lower().replace(' ', '_') + response = input(prompt).lower().replace(' ', '_').replace('-', '_') if response == os.path.basename(BASE_DIR).lower(): self.stdout.write('Invalid response. Please try again.') elif not response: diff --git a/tom_targets/views.py b/tom_targets/views.py index e53f72a22..eb9536f0a 100644 --- a/tom_targets/views.py +++ b/tom_targets/views.py @@ -292,7 +292,7 @@ def get_queryset(self, *args, **kwargs): :returns: Set of targets :rtype: QuerySet """ - return get_objects_for_user(self.request.user, 'tom_targets.change_target') + return get_objects_for_user(self.request.user, f'{Target._meta.app_label}.change_target') def get_form_class(self): """ From 5ab8cea2764d884b0b8413f869eeb6317aa24b0d Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 11 Apr 2024 16:39:07 -0700 Subject: [PATCH 20/69] fix api_view permissions --- tom_targets/api_views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tom_targets/api_views.py b/tom_targets/api_views.py index 861640595..c4043da70 100644 --- a/tom_targets/api_views.py +++ b/tom_targets/api_views.py @@ -6,7 +6,7 @@ from rest_framework import status from tom_targets.filters import TargetFilter -from tom_targets.models import TargetExtra, TargetName, TargetList +from tom_targets.models import TargetExtra, TargetName, TargetList, Target from tom_targets.serializers import TargetSerializer, TargetExtraSerializer, TargetNameSerializer, TargetListSerializer @@ -52,7 +52,7 @@ class TargetViewSet(ModelViewSet, PermissionListMixin): def get_queryset(self): permission_required = permissions_map.get(self.request.method) - return get_objects_for_user(self.request.user, f'tom_targets.{permission_required}') + return get_objects_for_user(self.request.user, f'{Target._meta.app_label}.{permission_required}') def create(self, request, *args, **kwargs): response = super().create(request, *args, **kwargs) @@ -82,7 +82,7 @@ class TargetNameViewSet(DestroyModelMixin, PermissionListMixin, RetrieveModelMix def get_queryset(self): permission_required = permissions_map.get(self.request.method) return TargetName.objects.filter( - target__in=get_objects_for_user(self.request.user, f'tom_targets.{permission_required}') + target__in=get_objects_for_user(self.request.user, f'{Target._meta.app_label}.{permission_required}') ) @@ -97,7 +97,7 @@ class TargetExtraViewSet(DestroyModelMixin, PermissionListMixin, RetrieveModelMi def get_queryset(self): permission_required = permissions_map.get(self.request.method) return TargetExtra.objects.filter( - target__in=get_objects_for_user(self.request.user, f'tom_targets.{permission_required}') + target__in=get_objects_for_user(self.request.user, f'{Target._meta.app_label}.{permission_required}') ) @@ -112,5 +112,5 @@ class TargetListViewSet(DestroyModelMixin, PermissionListMixin, RetrieveModelMix def get_queryset(self): permission_required = permissions_map.get(self.request.method) return TargetList.objects.filter( - target__in=get_objects_for_user(self.request.user, f'tom_targets.{permission_required}') + target__in=get_objects_for_user(self.request.user, f'{Target._meta.app_label}.{permission_required}') ) From 469a05623ec3bd686f509b38854260ebf4eab63c Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 11 Apr 2024 17:01:01 -0700 Subject: [PATCH 21/69] some finishing touches for tom_setup --- tom_setup/management/commands/tom_setup.py | 3 +-- tom_setup/templates/tom_setup/models.tmpl | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tom_setup/management/commands/tom_setup.py b/tom_setup/management/commands/tom_setup.py index f0b1098ca..667416d43 100644 --- a/tom_setup/management/commands/tom_setup.py +++ b/tom_setup/management/commands/tom_setup.py @@ -81,11 +81,10 @@ def create_project_dirs(self): ├── tmp ├── mytom │ ├── __init__.py - │ ├── models.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py - ├── myapp + ├── custom_code │ ├── __init__.py │ ├── migrations │ ├── admin.py diff --git a/tom_setup/templates/tom_setup/models.tmpl b/tom_setup/templates/tom_setup/models.tmpl index 2fc354e02..4e81083af 100644 --- a/tom_setup/templates/tom_setup/models.tmpl +++ b/tom_setup/templates/tom_setup/models.tmpl @@ -4,6 +4,9 @@ from tom_targets.base_models import BaseTarget class UserDefinedTarget(BaseTarget): + """ + A target with fields defined by a user. + """ class Meta: verbose_name = "target" From ecb1d83da31eb31eeac5a55565dcd47183f2d1bf Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 12 Apr 2024 10:57:00 -0700 Subject: [PATCH 22/69] update docs --- docs/targets/target_fields.rst | 146 ++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 1 deletion(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 59c416194..589fa6c7a 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -10,13 +10,157 @@ the redshift of your targets. You could then do a search for targets with a redshift less than or greater than a particular value, or use the redshift value to make decisions in your science code. +The TOM Toolkit currently supports two different methods for adding extra +fields to targets: + +Extending the Target Model +========================== +Users can extend the `Target` model by creating a custom target model in the app +where they store their custom code. This method is more flexible and allows for +more intuitive relationships between the new target fields and other code the user +may create. This method requires database migrations and a greater understanding of +Django models to implement. + +By default the TOM Toolkit will use the `tom_targets.BaseTarget` model as the target model, +but users can create their own target model by subclassing `tom_targets.BaseTarget` and adding +their own fields. The TOM Toolkit will then use the custom target model if it is defined +in the `BASE_TARGET_MODEL` setting of ``settings.py``. To implement this a user will first +have to edit a ``models.py`` file in their custom code app and define a custom target model. + +Subclassing `tom_targets.BaseTarget` will give the user access to all the fields and methods +of the `BaseTarget` model, but the user can also add their own fields and methods to the custom +target model. Fields from the `BaseTarget` model will be stored in a separate table from the custom +fields, and rely on separate migrations. See the +`Django documentation on multi-table inheritance. `__ + +Preparing your project for custom Target Models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The first thing your project will need is a custom app. If you already have a custom app +(usually called ``custom_code``) you can skip this section. You can read +about custom apps in the Django tutorial +`here `__, but +to quickly get started, the command to create a new app is as follows: + +.. code:: python + + ./manage.py startapp custom_code + +Where ``custom_code`` is the name of your app. You will also need to +ensure that ``custom_code`` is in your ``settings.py``. Append it to the +end of ``INSTALLED_APPS``: + +.. code:: python + + ... + INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + ... + 'tom_dataproducts', + 'custom_code', + ] + ... + +You should now have a directory within your TOM called ``custom_code``, +which looks like this: + +:: + + ├── custom_code + | ├── __init__.py + │ ├── admin.py + │ ├── apps.py + │ ├── models.py + │ ├── tests.py + │ └── views.py + ├── data + ├── db.sqlite3 + ├── manage.py + ├── mytom + │ ├── __init__.py + │ ├── settings.py + │ ├── urls.py + │ └── wsgi.py + ├── static + ├── templates + └── tmp + +Editing ``models.py`` +~~~~~~~~~~~~~~~~~~~~~ +First you will need to create a custom target model in the `models.py` file of your custom app. +The following is an example of a custom target model that adds a boolean field and a number field: + +.. code:: python + + from django.db import models + + from tom_targets.base_models import BaseTarget + + + class UserDefinedTarget(BaseTarget): + example_bool = models.BooleanField(default=False) + example_number = models.FloatField(default=0) + + # Set Hidden Fields + example_bool.hidden = True + + class Meta: + verbose_name = "target" + permissions = ( + ('view_target', 'View Target'), + ('add_target', 'Add Target'), + ('change_target', 'Change Target'), + ('delete_target', 'Delete Target'), + ) + +The model name, `UserDefinedTarget` in the example, can be replaced by whatever CamelCase name you want, but +it must be a subclass of `tom_targets.BaseTarget`. The permissions in the class Meta are required for the +TOM Toolkit to work properly. The `hidden` attribute can be set to `True` to hide the field from the target +detail page. + +Editing ``settings.py`` +~~~~~~~~~~~~~~~~~~~~~~~ +Next you will need to tell the TOM Toolkit to use your custom target model. In the `settings.py` file of your +project, you will need to add the following line: + +.. code:: python + + BASE_TARGET_MODEL = 'custom_code.models.UserDefinedTarget' + +Changing `custom_code` to the name of your custom app and `UserDefinedTarget` to the name of your custom target model. + +Creating Migrations +~~~~~~~~~~~~~~~~~~~~ +After you have created your custom target model, you will need to create a migration for it. To do this, run the +following command: + +.. code:: python + + ./manage.py makemigrations + +This will create a migration file in the `migrations` directory of your custom app. You can then apply the migration +by running: + +.. code:: python + + ./manage.py migrate + +This will build the appropriate tables in your database for your custom target model. + +Adding ``Extra Fields`` +======================= +If a user does not want to create a custom target model, they can use the `EXTRA_FIELDS` +setting to add extra fields to the `Target` model. This method is simpler and does not require +any database migrations, but is less flexible than creating a custom target model. + **Note**: There is a performance hit when using extra fields. Try to use the built in fields whenever possible. Enabling extra fields ~~~~~~~~~~~~~~~~~~~~~ -To start, find the ``EXTRA_FIELDS`` definition in your ``settings.py``: +To start, find the `EXTRA_FIELDS` definition in your ``settings.py``: .. code:: python From 97a5286ed59f5eab10468a81a56500488b30bd1b Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 12 Apr 2024 11:39:34 -0700 Subject: [PATCH 23/69] add extra space --- docs/targets/target_fields.rst | 2 +- tom_targets/models.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 589fa6c7a..3ad921b2f 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -11,7 +11,7 @@ with a redshift less than or greater than a particular value, or use the redshift value to make decisions in your science code. The TOM Toolkit currently supports two different methods for adding extra -fields to targets: +fields to targets: Extending Target Models and adding Extra Fields. Extending the Target Model ========================== diff --git a/tom_targets/models.py b/tom_targets/models.py index 4b92ea763..9fe4f7e18 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -26,6 +26,7 @@ def get_target_base_model(): Target = get_target_base_model() + class TargetName(models.Model): """ Class representing an alternative name for a ``Target``. From c209fe8aa0169f85e482c79d5dc10db1794e14ce Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 12 Apr 2024 17:30:42 -0700 Subject: [PATCH 24/69] add management command to docs --- docs/targets/target_fields.rst | 47 ++++++++++++++++++++++ tom_setup/management/commands/tom_setup.py | 18 +++++++-- tom_setup/templates/tom_setup/models.tmpl | 26 ++++++------ 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 3ad921b2f..bfb07fb6d 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -148,6 +148,53 @@ by running: This will build the appropriate tables in your database for your custom target model. +Convert old targets to new model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have existing targets in your database, you will need to convert them to the new model. This can be done by +running a version of the following code. We incorporate this into a management command to make it easier to run. + +Create a new file in your custom app called ``management/commands/convert_targets.py`` and add the following code: + +.. code:: python + + from django.core.management.base import BaseCommand + + from tom_targets.base_models import BaseTarget + from tom_targets.models import Target + + + class Command(BaseCommand): + """ + Core code based on information found at + https://code.djangoproject.com/ticket/7623 + """ + + help = 'A helper command to convert existing BaseTargets to UserDefinedTargets.' + + def handle(self, *args, **options): + if Target != BaseTarget and issubclass(Target, BaseTarget): + self.stdout.write(f'{Target} is a subclass of BaseTarget, updating existing Targets.') + base_targets = BaseTarget.objects.all() + targets = Target.objects.all() + for base_target in base_targets: + if not targets.filter(pk=base_target.pk).exists(): + self.stdout.write(f'Updating {base_target}...') + target = Target(basetarget_ptr_id=base_target.pk) + target.__dict__.update(base_target.__dict__) + target.save() + self.stdout.write(f'{Target.objects.count()} Targets updated.') + + return + +Once this file is created, you can run the following command to convert your old targets to the new model: + +.. code:: python + + ./manage.py convert_targets + + + Adding ``Extra Fields`` ======================= If a user does not want to create a custom target model, they can use the `EXTRA_FIELDS` diff --git a/tom_setup/management/commands/tom_setup.py b/tom_setup/management/commands/tom_setup.py index 667416d43..1f3e00f35 100644 --- a/tom_setup/management/commands/tom_setup.py +++ b/tom_setup/management/commands/tom_setup.py @@ -62,10 +62,9 @@ def create_custom_code_app(self): response = input(prompt).lower().replace(' ', '_').replace('-', '_') if response == os.path.basename(BASE_DIR).lower(): self.stdout.write('Invalid response. Please try again.') - elif not response: - self.context['CUSTOM_CODE_APP_NAME'] = 'custom_code' - break else: + if not response: + response = 'custom_code' self.context['CUSTOM_CODE_APP_NAME'] = response break call_command('startapp', response) @@ -86,6 +85,8 @@ def create_project_dirs(self): │ └── wsgi.py ├── custom_code │ ├── __init__.py + │ └── management + │ └── commands │ ├── migrations │ ├── admin.py │ ├── apps.py @@ -112,6 +113,17 @@ def create_project_dirs(self): os.mkdir(static_dir) except FileExistsError: pass + # --- set up management command directories for custom code app --- + custom_code_app_dir = os.path.join(BASE_DIR, self.context.get('CUSTOM_CODE_APP_NAME', 'custom_code')) + management_dir = os.path.join(custom_code_app_dir, 'management') + try: + os.mkdir(management_dir) + except FileExistsError: + pass + try: + os.mkdir(os.path.join(management_dir, 'commands')) + except FileExistsError: + pass # os.mknod requires superuser permissions on osx, so create a blank file instead try: open(os.path.join(static_dir, '.keep'), 'w').close() diff --git a/tom_setup/templates/tom_setup/models.tmpl b/tom_setup/templates/tom_setup/models.tmpl index 4e81083af..ba22a8da6 100644 --- a/tom_setup/templates/tom_setup/models.tmpl +++ b/tom_setup/templates/tom_setup/models.tmpl @@ -3,16 +3,16 @@ from django.db import models from tom_targets.base_models import BaseTarget -class UserDefinedTarget(BaseTarget): - """ - A target with fields defined by a user. - """ - - class Meta: - verbose_name = "target" - permissions = ( - ('view_target', 'View Target'), - ('add_target', 'Add Target'), - ('change_target', 'Change Target'), - ('delete_target', 'Delete Target'), - ) +#class UserDefinedTarget(BaseTarget): +# """ +# A target with fields defined by a user. +# """ +# +# class Meta: +# verbose_name = "target" +# permissions = ( +# ('view_target', 'View Target'), +# ('add_target', 'Add Target'), +# ('change_target', 'Change Target'), +# ('delete_target', 'Delete Target'), +# ) From 3797ad580f26b5a8f9cdb7dd5b5c7a8c15430395 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 12 Apr 2024 17:34:11 -0700 Subject: [PATCH 25/69] migrate --- ...get_basetarget_alter_basetarget_options.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tom_targets/migrations/0021_rename_target_basetarget_alter_basetarget_options.py diff --git a/tom_targets/migrations/0021_rename_target_basetarget_alter_basetarget_options.py b/tom_targets/migrations/0021_rename_target_basetarget_alter_basetarget_options.py new file mode 100644 index 000000000..13e2100e0 --- /dev/null +++ b/tom_targets/migrations/0021_rename_target_basetarget_alter_basetarget_options.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.10 on 2024-04-13 00:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tom_observations', '0012_auto_20210205_1819'), + ('tom_dataproducts', '0012_alter_reduceddatum_data_product_and_more'), + ('tom_targets', '0020_alter_targetname_created_alter_targetname_modified'), + ] + + operations = [ + migrations.RenameModel( + old_name='Target', + new_name='BaseTarget', + ), + migrations.AlterModelOptions( + name='basetarget', + options={'permissions': (('view_target', 'View Target'), ('add_target', 'Add Target'), ('change_target', 'Change Target'), ('delete_target', 'Delete Target')), 'verbose_name': 'target'}, + ), + ] From a4ad9544c9716a9bbdc3b407791685ca9cce61a1 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Mon, 15 Apr 2024 18:46:49 -0700 Subject: [PATCH 26/69] add line numbers and copy button to readthedocs. --- docs/conf.py | 3 +- docs/targets/target_fields.rst | 8 +- poetry.lock | 217 +++++++++++++++++++-------------- pyproject.toml | 1 + 4 files changed, 135 insertions(+), 94 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b558daad4..8641c5189 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,7 +52,8 @@ 'sphinx.ext.autosectionlabel', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', - 'sphinx_panels' + 'sphinx_panels', + 'sphinx_copybutton', ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index bfb07fb6d..08ee01f6d 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -91,7 +91,9 @@ Editing ``models.py`` First you will need to create a custom target model in the `models.py` file of your custom app. The following is an example of a custom target model that adds a boolean field and a number field: -.. code:: python +.. code-block:: python + :caption: models.py + :linenos: from django.db import models @@ -156,7 +158,9 @@ running a version of the following code. We incorporate this into a management c Create a new file in your custom app called ``management/commands/convert_targets.py`` and add the following code: -.. code:: python +.. code-block:: python + :caption: convert_targets.py + :linenos: from django.core.management.base import BaseCommand diff --git a/poetry.lock b/poetry.lock index c766ecc9a..3607e8ec1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -373,6 +373,21 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +[[package]] +name = "backports-tarfile" +version = "1.0.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.0.0-py3-none-any.whl", hash = "sha256:bcd36290d9684beb524d3fe74f4a2db056824c47746583f090b8e55daf0776e4"}, + {file = "backports.tarfile-1.0.0.tar.gz", hash = "sha256:2688f159c21afd56a07b75f01306f9f52c79aebcc5f4a117fb8fbb4445352c75"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "backports-zoneinfo" version = "0.2.1" @@ -988,13 +1003,13 @@ doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] name = "faker" -version = "24.4.0" +version = "24.9.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-24.4.0-py3-none-any.whl", hash = "sha256:998c29ee7d64429bd59204abffa9ba11f784fb26c7b9df4def78d1a70feb36a7"}, - {file = "Faker-24.4.0.tar.gz", hash = "sha256:a5ddccbe97ab691fad6bd8036c31f5697cfaa550e62e000078d1935fa8a7ec2e"}, + {file = "Faker-24.9.0-py3-none-any.whl", hash = "sha256:97c7874665e8eb7b517f97bf3b59f03bf3f07513fe2c159e98b6b9ea6b9f2b3d"}, + {file = "Faker-24.9.0.tar.gz", hash = "sha256:73b1e7967b0ceeac42fc99a8c973bb49e4499cc4044d20d17ab661d5cb7eda1d"}, ] [package.dependencies] @@ -1216,13 +1231,13 @@ lxml = ["lxml"] [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -1275,36 +1290,39 @@ testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "p [[package]] name = "jaraco-classes" -version = "3.3.1" +version = "3.4.0" description = "Utility functions for Python class constructs" optional = false python-versions = ">=3.8" files = [ - {file = "jaraco.classes-3.3.1-py3-none-any.whl", hash = "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206"}, - {file = "jaraco.classes-3.3.1.tar.gz", hash = "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30"}, + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, ] [package.dependencies] more-itertools = "*" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jaraco-context" -version = "4.3.0" -description = "Context managers by jaraco" +version = "5.3.0" +description = "Useful decorators and context managers" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "jaraco.context-4.3.0-py3-none-any.whl", hash = "sha256:5d9e95ca0faa78943ed66f6bc658dd637430f16125d86988e77844c741ff2f11"}, - {file = "jaraco.context-4.3.0.tar.gz", hash = "sha256:4dad2404540b936a20acedec53355bdaea223acb88fd329fa6de9261c941566e"}, + {file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"}, + {file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"}, ] +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jaraco-functools" @@ -1390,13 +1408,13 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "keyring" -version = "25.0.0" +version = "25.1.0" description = "Store and access your passwords safely." optional = false python-versions = ">=3.8" files = [ - {file = "keyring-25.0.0-py3-none-any.whl", hash = "sha256:9a15cd280338920388e8c1787cb8792b9755dabb3e7c61af5ac1f8cd437cefde"}, - {file = "keyring-25.0.0.tar.gz", hash = "sha256:fc024ed53c7ea090e30723e6bd82f58a39dc25d9a6797d866203ecd0ee6306cb"}, + {file = "keyring-25.1.0-py3-none-any.whl", hash = "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427"}, + {file = "keyring-25.1.0.tar.gz", hash = "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893"}, ] [package.dependencies] @@ -1412,7 +1430,7 @@ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] completion = ["shtab (>=1.1.0)"] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "markdown" @@ -1734,13 +1752,13 @@ files = [ [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] @@ -1801,22 +1819,22 @@ test = ["pytest", "pytest-doctestplus (>=0.7)"] [[package]] name = "pyerfa" -version = "2.0.1.1" +version = "2.0.1.4" description = "Python bindings for ERFA" optional = false python-versions = ">=3.9" files = [ - {file = "pyerfa-2.0.1.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:1ce322ac30673c2aeb0ee22ced4938c1e9e26db0cbe175912a213aaff42383df"}, - {file = "pyerfa-2.0.1.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:67dfc00dcdea87a9b3c0bb4596fb0cfb54ee9c1c75fdcf19411d1029a18f6eec"}, - {file = "pyerfa-2.0.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34ee545780246fb0d1d3f7e46a6daa152be06a26b2d27fbfe309cab9ab488ea7"}, - {file = "pyerfa-2.0.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db85db72ab352da6ffc790e41209d8f41feb5b175d682cf1f0e3e60e9e5cdf8"}, - {file = "pyerfa-2.0.1.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c50b7cdb005632931b7b56a679cf25361ed6b3aa7c21e491e65cc89cb337e66a"}, - {file = "pyerfa-2.0.1.1-cp39-abi3-win32.whl", hash = "sha256:30649047b7a8ce19f43e4d18a26b8a44405a6bb406df16c687330a3b937723b2"}, - {file = "pyerfa-2.0.1.1-cp39-abi3-win_amd64.whl", hash = "sha256:94df7566ce5a5abb14e2dd1fe879204390639e9a76383ec27f10598eb24be760"}, - {file = "pyerfa-2.0.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0e95cf3d11f76f473bf011980e9ea367ca7e68ca675d8b32499814fb6e387d4c"}, - {file = "pyerfa-2.0.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08b5abb90b34e819c1fca69047a76c0d344cb0c8fe4f7c8773f032d8afd623b4"}, - {file = "pyerfa-2.0.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1c0c1efa701cab986aa58d03c58f77e47ea1898bff2684377d29580a055f836a"}, - {file = "pyerfa-2.0.1.1.tar.gz", hash = "sha256:dbac74ef8d3d3b0f22ef0ad3bbbdb30b2a9e10570b1fa5a98be34c7be36c9a6b"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ff112353944bf705342741f2fe41674f97154a302b0295eaef7381af92ad2b3a"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:900b266a3862baa9560d6b1b184dcc14e0e76d550ff70d32336d3989b2ed18ca"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:610d2bc314e140d876b93b1287c7c81685434873c8700cc3e1596193f77d1071"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e4508dd7ffd7b27b7f67168643764454887e990ca9e4584824f0e3ab5884c0f"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:83a44ba84ebfc3244412ecbf1065c087c382da84f1c3eee1f2a0638d9046ac96"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-win32.whl", hash = "sha256:46d3bed0ac666f08d8364b34a00b8c6595358d6c4f4532da8d13fac0e5227baa"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-win_amd64.whl", hash = "sha256:bc3cf45967ac1af77a777deb050fb08bbc75256dd97ca6005e4d385358b7af40"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88a8d0f3608a66871615bd168fcddf674dce9f7568c239a03cf8d9936161d032"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9045e9f786c76cb55da86ada3405c378c32b88f6e3c6296cb288496ab374b068"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:39cf838c9a21e40d4e3183bead65b3ce6af763c4a727f87d84909c9be7d3a33c"}, + {file = "pyerfa-2.0.1.4.tar.gz", hash = "sha256:acb8a6713232ea35c04bc6e40ac4e461dfcc817d395ef2a3c8051c1a33249dd3"}, ] [package.dependencies] @@ -2153,18 +2171,18 @@ doc = ["Sphinx", "sphinx-rtd-theme"] [[package]] name = "setuptools" -version = "69.2.0" +version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2260,6 +2278,24 @@ docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"] test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +description = "Add a copy button to each of your code cells." +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[package.dependencies] +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] + [[package]] name = "sphinx-panels" version = "0.6.0" @@ -2405,19 +2441,18 @@ test = ["pytest"] [[package]] name = "sqlparse" -version = "0.4.4" +version = "0.5.0" description = "A non-validating SQL parser." optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, - {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, + {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, + {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, ] [package.extras] -dev = ["build", "flake8"] +dev = ["build", "hatch"] doc = ["sphinx"] -test = ["pytest", "pytest-cov"] [[package]] name = "tenacity" @@ -2435,13 +2470,13 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"] [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] @@ -2574,58 +2609,58 @@ test = ["zope.testrunner"] [[package]] name = "zope-interface" -version = "6.2" +version = "6.3" description = "Interfaces for Python" optional = false python-versions = ">=3.7" files = [ - {file = "zope.interface-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab"}, - {file = "zope.interface-6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328"}, - {file = "zope.interface-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd"}, - {file = "zope.interface-6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037"}, - {file = "zope.interface-6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac"}, - {file = "zope.interface-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe"}, - {file = "zope.interface-6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48"}, - {file = "zope.interface-6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b"}, - {file = "zope.interface-6.2-cp312-cp312-win_amd64.whl", hash = "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe"}, - {file = "zope.interface-6.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b"}, - {file = "zope.interface-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550"}, - {file = "zope.interface-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532"}, - {file = "zope.interface-6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3"}, - {file = "zope.interface-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0"}, - {file = "zope.interface-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099"}, - {file = "zope.interface-6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a"}, - {file = "zope.interface-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1"}, - {file = "zope.interface-6.2.tar.gz", hash = "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565"}, + {file = "zope.interface-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021"}, + {file = "zope.interface-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785"}, + {file = "zope.interface-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a"}, + {file = "zope.interface-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21"}, + {file = "zope.interface-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1"}, + {file = "zope.interface-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb"}, + {file = "zope.interface-6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39"}, + {file = "zope.interface-6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299"}, + {file = "zope.interface-6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130"}, + {file = "zope.interface-6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10"}, + {file = "zope.interface-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e"}, + {file = "zope.interface-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061"}, + {file = "zope.interface-6.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5"}, + {file = "zope.interface-6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b"}, + {file = "zope.interface-6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e"}, + {file = "zope.interface-6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920"}, + {file = "zope.interface-6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c"}, + {file = "zope.interface-6.3-cp312-cp312-win_amd64.whl", hash = "sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5"}, + {file = "zope.interface-6.3-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85"}, + {file = "zope.interface-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e"}, + {file = "zope.interface-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e"}, + {file = "zope.interface-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d"}, + {file = "zope.interface-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12"}, + {file = "zope.interface-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86"}, + {file = "zope.interface-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0"}, + {file = "zope.interface-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91"}, + {file = "zope.interface-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5"}, + {file = "zope.interface-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1"}, + {file = "zope.interface-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af"}, + {file = "zope.interface-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92"}, + {file = "zope.interface-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf"}, + {file = "zope.interface-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83"}, + {file = "zope.interface-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c"}, + {file = "zope.interface-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9"}, + {file = "zope.interface-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6"}, + {file = "zope.interface-6.3.tar.gz", hash = "sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a"}, ] [package.dependencies] setuptools = "*" [package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx_rtd_theme"] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<3.12" -content-hash = "961549542fbb45b6ed4ead4d1a3d892e81d8853c2b12fdde5b0d5203f2974ec6" +content-hash = "b3bf72d2aaf2ff0da8ba0e54eff73953bb75db33aa770e6230d96d7870a84ada" diff --git a/pyproject.toml b/pyproject.toml index f6e83af3a..fb9989d9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ recommonmark = "~0.7" sphinx = ">=4,<8" sphinx-rtd-theme = ">=1.0,<2.1" sphinx-panels = "~0.6" +sphinx-copybutton = "~0.5" [tool.poetry.group.coverage.dependencies] coverage = "~6" # coveralls needs ~6 even though 7.3.2 is latest From bb04d5a6aad9b0c09d7754d59b22feb7a2fe53e7 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 16 Apr 2024 12:00:48 -0700 Subject: [PATCH 27/69] make a custom exception handler for target API. --- tom_targets/api_views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tom_targets/api_views.py b/tom_targets/api_views.py index c4043da70..c8197663f 100644 --- a/tom_targets/api_views.py +++ b/tom_targets/api_views.py @@ -1,8 +1,10 @@ from django_filters import rest_framework as drf_filters +from django.http import Http404 from guardian.mixins import PermissionListMixin from guardian.shortcuts import get_objects_for_user from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework.response import Response from rest_framework import status from tom_targets.filters import TargetFilter @@ -70,6 +72,13 @@ def update(self, request, *args, **kwargs): response.data['message'] = 'Target successfully updated.' return response + def handle_exception(self, exc): + if isinstance(exc, Http404): + return Response({'detail': 'No Target matches the given query.'}, + status=status.HTTP_404_NOT_FOUND) + + return super(TargetViewSet, self).handle_exception(exc) + class TargetNameViewSet(DestroyModelMixin, PermissionListMixin, RetrieveModelMixin, GenericViewSet): """ From 2dde1ba14bf40d37474ce210e0bd5d1d844292fc Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 16 Apr 2024 12:11:58 -0700 Subject: [PATCH 28/69] add docstring to error handler. --- tom_targets/api_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tom_targets/api_views.py b/tom_targets/api_views.py index c8197663f..8602a2522 100644 --- a/tom_targets/api_views.py +++ b/tom_targets/api_views.py @@ -73,6 +73,8 @@ def update(self, request, *args, **kwargs): return response def handle_exception(self, exc): + """Create Custom Error Message for Http404 errors because Target can have different names based on the supplied + Model.""" if isinstance(exc, Http404): return Response({'detail': 'No Target matches the given query.'}, status=status.HTTP_404_NOT_FOUND) From 1280354e790bfd041ef1f236f87ea4b8dcf2db6c Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 16 Apr 2024 16:30:16 -0700 Subject: [PATCH 29/69] update a few more permissions to be based on the target app --- tom_common/templatetags/tom_common_extras.py | 4 +++- tom_targets/views.py | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tom_common/templatetags/tom_common_extras.py b/tom_common/templatetags/tom_common_extras.py index 4e1aa4ecb..d38966aca 100644 --- a/tom_common/templatetags/tom_common_extras.py +++ b/tom_common/templatetags/tom_common_extras.py @@ -5,6 +5,8 @@ from django_comments.models import Comment from guardian.shortcuts import get_objects_for_user +from tom_targets.models import Target + register = template.Library() @@ -44,7 +46,7 @@ def recent_comments(context, limit=10): Comments will only be displayed for targets which the logged-in user has permission to view. """ user = context['request'].user - targets_for_user = get_objects_for_user(user, 'tom_targets.view_target') + targets_for_user = get_objects_for_user(user, f'{Target._meta.app_label}.view_target') # In django-contrib-comments, the Comment model has a field ``object_pk`` which refers to the primary key # of the object it is related to, i.e., a comment on a ``Target`` has an ``object_pk`` corresponding with the diff --git a/tom_targets/views.py b/tom_targets/views.py index eb9536f0a..268a92e67 100644 --- a/tom_targets/views.py +++ b/tom_targets/views.py @@ -92,7 +92,7 @@ def get(self, request, *args, **kwargs): # Tests fail without distinct but it works in practice, it is unclear as to why # The Django query planner shows different results between in practice and unit tests # django-guardian related querying is present in the test planner, but not in practice - targets = get_objects_for_user(request.user, 'tom_targets.view_target').filter( + targets = get_objects_for_user(request.user, f'{Target._meta.app_label}.view_target').filter( Q(name__icontains=target_name) | Q(aliases__name__icontains=target_name) ).distinct() if targets.count() == 1: @@ -581,7 +581,7 @@ def post(self, request, *args, **kwargs): except Exception as e: messages.error(request, 'Cannot find the target group with id={}; {}'.format(grouping_id, e)) return redirect(reverse('tom_targets:list') + '?' + query_string) - if not request.user.has_perm('tom_targets.view_targetlist', grouping_object): + if not request.user.has_perm(f'{Target._meta.app_label}.view_targetlist', grouping_object): messages.error(request, 'Permission denied.') return redirect(reverse('tom_targets:list') + '?' + query_string) @@ -611,7 +611,7 @@ class TargetGroupingView(PermissionListMixin, ListView): """ View that handles the display of ``TargetList`` objects, also known as target groups. Requires authorization. """ - permission_required = 'tom_targets.view_targetlist' + permission_required = f'{Target._meta.app_label}.view_targetlist' template_name = 'tom_targets/target_grouping.html' model = TargetList paginate_by = 25 @@ -653,9 +653,9 @@ def form_valid(self, form): """ obj = form.save(commit=False) obj.save() - assign_perm('tom_targets.view_targetlist', self.request.user, obj) - assign_perm('tom_targets.change_targetlist', self.request.user, obj) - assign_perm('tom_targets.delete_targetlist', self.request.user, obj) + assign_perm(f'{Target._meta.app_label}.view_targetlist', self.request.user, obj) + assign_perm(f'{Target._meta.app_label}.change_targetlist', self.request.user, obj) + assign_perm(f'{Target._meta.app_label}.delete_targetlist', self.request.user, obj) return super().form_valid(form) From 8291bdc93e570867c2da91d9f96e40c70483bdf1 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 16 Apr 2024 16:57:01 -0700 Subject: [PATCH 30/69] change BASE_TARGET_MODEL to TARGET_MODEL_CLASS --- docs/targets/target_fields.rst | 16 ++++++++-------- tom_setup/templates/tom_setup/settings.tmpl | 8 ++------ tom_targets/models.py | 4 ++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 08ee01f6d..5e7f42347 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -15,21 +15,21 @@ fields to targets: Extending Target Models and adding Extra Fields. Extending the Target Model ========================== -Users can extend the `Target` model by creating a custom target model in the app +Users can extend the ``Target`` model by creating a custom target model in the app where they store their custom code. This method is more flexible and allows for more intuitive relationships between the new target fields and other code the user may create. This method requires database migrations and a greater understanding of Django models to implement. -By default the TOM Toolkit will use the `tom_targets.BaseTarget` model as the target model, -but users can create their own target model by subclassing `tom_targets.BaseTarget` and adding +By default the TOM Toolkit will use the ``tom_targets.BaseTarget`` model as the target model, +but users can create their own target model by subclassing ``tom_targets.BaseTarget`` and adding their own fields. The TOM Toolkit will then use the custom target model if it is defined -in the `BASE_TARGET_MODEL` setting of ``settings.py``. To implement this a user will first +in the ``TARGET_MODEL_CLASS`` setting of ``settings.py``. To implement this a user will first have to edit a ``models.py`` file in their custom code app and define a custom target model. -Subclassing `tom_targets.BaseTarget` will give the user access to all the fields and methods -of the `BaseTarget` model, but the user can also add their own fields and methods to the custom -target model. Fields from the `BaseTarget` model will be stored in a separate table from the custom +Subclassing ``tom_targets.BaseTarget`` will give the user access to all the fields and methods +of the ``BaseTarget`` model, but the user can also add their own fields and methods to the custom +target model. Fields from the ``BaseTarget`` model will be stored in a separate table from the custom fields, and rely on separate migrations. See the `Django documentation on multi-table inheritance. `__ @@ -128,7 +128,7 @@ project, you will need to add the following line: .. code:: python - BASE_TARGET_MODEL = 'custom_code.models.UserDefinedTarget' + TARGET_MODEL_CLASS = 'custom_code.models.UserDefinedTarget' Changing `custom_code` to the name of your custom app and `UserDefinedTarget` to the name of your custom target model. diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index 9f5732c32..20073e021 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -33,7 +33,7 @@ ALLOWED_HOSTS = [] # Application definition -TOM_NAME = 'TOM Toolkit' +TOM_NAME = '{{ PROJECT_NAME }}' INSTALLED_APPS = [ 'django.contrib.admin', @@ -195,7 +195,7 @@ CACHES = { TARGET_TYPE = '{{ TARGET_TYPE }}' # Set to the full path of a custom target model to extend the BaseTarget Model with custom fields. -# BASE_TARGET_MODEL = '{{ CUSTOM_CODE_APP_NAME }}.models.UserDefinedTarget' +# TARGET_MODEL_CLASS = '{{ CUSTOM_CODE_APP_NAME }}.models.UserDefinedTarget' FACILITIES = { 'LCO': { @@ -242,10 +242,6 @@ DATA_PROCESSORS = { 'spectroscopy': 'tom_dataproducts.processors.spectroscopy_processor.SpectroscopyProcessor', } -BASE_TARGET_MODELS = [ - '{{ PROJECT_NAME }}.models.UserDefinedTarget', -] - TOM_FACILITY_CLASSES = [ 'tom_observations.facilities.lco.LCOFacility', 'tom_observations.facilities.gemini.GEMFacility', diff --git a/tom_targets/models.py b/tom_targets/models.py index 9fe4f7e18..e56ddc257 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -15,13 +15,13 @@ def get_target_base_model(): base_class = BaseTarget try: - BASE_TARGET_MODEL = settings.BASE_TARGET_MODEL + TARGET_MODEL_CLASS = settings.TARGET_MODEL_CLASS clazz = import_string(BASE_TARGET_MODEL) return clazz except AttributeError: return base_class except ImportError: - raise ImportError(f'Could not import {BASE_TARGET_MODEL}. Did you provide the correct path?') + raise ImportError(f'Could not import {TARGET_MODEL_CLASS}. Did you provide the correct path?') Target = get_target_base_model() From 9336f029ef2a0dcd028fc7e049c511624ae0979e Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 16 Apr 2024 17:17:12 -0700 Subject: [PATCH 31/69] missed one --- tom_targets/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index e56ddc257..ec0643768 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -16,7 +16,7 @@ def get_target_base_model(): base_class = BaseTarget try: TARGET_MODEL_CLASS = settings.TARGET_MODEL_CLASS - clazz = import_string(BASE_TARGET_MODEL) + clazz = import_string(TARGET_MODEL_CLASS) return clazz except AttributeError: return base_class From c830e996c5bdbcced536bd6bef02209ac671e260 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 17 Apr 2024 11:40:12 -0700 Subject: [PATCH 32/69] add comments to management command. --- docs/targets/target_fields.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 5e7f42347..40147d050 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -177,15 +177,18 @@ Create a new file in your custom app called ``management/commands/convert_target help = 'A helper command to convert existing BaseTargets to UserDefinedTargets.' def handle(self, *args, **options): + # Make sure Target is a subclass of BaseTarget if Target != BaseTarget and issubclass(Target, BaseTarget): self.stdout.write(f'{Target} is a subclass of BaseTarget, updating existing Targets.') base_targets = BaseTarget.objects.all() targets = Target.objects.all() for base_target in base_targets: + # If the base_target is not already in the new target model, update it + # Note: subclassed models share a PK with their parent if not targets.filter(pk=base_target.pk).exists(): self.stdout.write(f'Updating {base_target}...') - target = Target(basetarget_ptr_id=base_target.pk) - target.__dict__.update(base_target.__dict__) + target = Target(basetarget_ptr_id=base_target.pk) # Create a new target with the base_target PK + target.__dict__.update(base_target.__dict__) # add base_target fields to target dictionary target.save() self.stdout.write(f'{Target.objects.count()} Targets updated.') From f550b8c8848a4171fa8e32e9ea5a29634221b943 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 17 Apr 2024 11:46:11 -0700 Subject: [PATCH 33/69] add concluson to docs --- docs/targets/target_fields.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 40147d050..a5dee79fc 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -200,7 +200,12 @@ Once this file is created, you can run the following command to convert your old ./manage.py convert_targets +Once this command is run, all of your old targets will be converted to the new model, but will not have the new fields +filled in. You will need to fill in these fields manually, but once you do any non-hidden fields will be displayed on +the target detail page. +Any fields added in this way are fully accessible in the TOM Toolkit as ``Target``, and can be used in the same way +as the built-in fields from any custom code you write, the API, or from the admin interface. Adding ``Extra Fields`` ======================= From fee2ee883f747c14cc35b4b56de59d95c66bfda8 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 17 Apr 2024 11:51:18 -0700 Subject: [PATCH 34/69] add form field comments --- tom_targets/forms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 3b843112b..965671299 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -113,6 +113,7 @@ def __init__(self, *args, **kwargs): self.fields[field].required = True class Meta(TargetForm.Meta): + # Include Sidereal Fields and User defined fields that are not included in the Base Target model. fields = SIDEREAL_FIELDS + [field.name for field in Target._meta.get_fields() if field.name not in SIDEREAL_FIELDS + IGNORE_FIELDS + NON_SIDEREAL_FIELDS] @@ -145,6 +146,7 @@ def clean(self): ) class Meta(TargetForm.Meta): + # Include Non-Sidereal Fields and User defined fields that are not included in the Base Target model. fields = NON_SIDEREAL_FIELDS + [field.name for field in Target._meta.get_fields() if field.name not in SIDEREAL_FIELDS + IGNORE_FIELDS + NON_SIDEREAL_FIELDS] From 568c80674d56f4bb6d9ddd55908c9ccc287b2915 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 17 Apr 2024 12:04:08 -0700 Subject: [PATCH 35/69] docstring for get_target_model_class --- tom_targets/models.py | 5 +++-- tom_targets/views.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tom_targets/models.py b/tom_targets/models.py index ec0643768..0c637a1ad 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -12,7 +12,8 @@ logger = logging.getLogger(__name__) -def get_target_base_model(): +def get_target_model_class(): + """Function to retrieve the target model class from settings.py. If not found, returns the default BaseTarget.""" base_class = BaseTarget try: TARGET_MODEL_CLASS = settings.TARGET_MODEL_CLASS @@ -24,7 +25,7 @@ def get_target_base_model(): raise ImportError(f'Could not import {TARGET_MODEL_CLASS}. Did you provide the correct path?') -Target = get_target_base_model() +Target = get_target_model_class() class TargetName(models.Model): diff --git a/tom_targets/views.py b/tom_targets/views.py index 268a92e67..b05aed1de 100644 --- a/tom_targets/views.py +++ b/tom_targets/views.py @@ -106,6 +106,7 @@ class TargetCreateView(LoginRequiredMixin, CreateView): View for creating a Target. Requires authentication. """ + # Target Views require explicit template names since the Model Class names are variable. template_name = 'tom_targets/target_form.html' model = Target fields = '__all__' From 932678a2f4e61cfa6b68789b2f46376fd5b50b49 Mon Sep 17 00:00:00 2001 From: "William (Lindy) Lindstrom" Date: Wed, 17 Apr 2024 20:52:02 +0100 Subject: [PATCH 36/69] denote code literals with monospace, rather than italics --- docs/targets/target_fields.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index a5dee79fc..b11c5ad5c 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -88,7 +88,7 @@ which looks like this: Editing ``models.py`` ~~~~~~~~~~~~~~~~~~~~~ -First you will need to create a custom target model in the `models.py` file of your custom app. +First you will need to create a custom target model in the ``models.py`` file of your custom app. The following is an example of a custom target model that adds a boolean field and a number field: .. code-block:: python @@ -116,21 +116,21 @@ The following is an example of a custom target model that adds a boolean field a ('delete_target', 'Delete Target'), ) -The model name, `UserDefinedTarget` in the example, can be replaced by whatever CamelCase name you want, but -it must be a subclass of `tom_targets.BaseTarget`. The permissions in the class Meta are required for the -TOM Toolkit to work properly. The `hidden` attribute can be set to `True` to hide the field from the target +The model name, ``UserDefinedTarget`` in the example, can be replaced by whatever CamelCase name you want, but +it must be a subclass of ``tom_targets.BaseTarget``. The permissions in the class Meta are required for the +TOM Toolkit to work properly. The ``hidden`` attribute can be set to ``True`` to hide the field from the target detail page. Editing ``settings.py`` ~~~~~~~~~~~~~~~~~~~~~~~ -Next you will need to tell the TOM Toolkit to use your custom target model. In the `settings.py` file of your +Next you will need to tell the TOM Toolkit to use your custom target model. In the ``settings.py`` file of your project, you will need to add the following line: .. code:: python TARGET_MODEL_CLASS = 'custom_code.models.UserDefinedTarget' -Changing `custom_code` to the name of your custom app and `UserDefinedTarget` to the name of your custom target model. +Changing ``c`ustom_code`` to the name of your custom app and ``UserDefinedTarget`` to the name of your custom target model. Creating Migrations ~~~~~~~~~~~~~~~~~~~~ @@ -141,7 +141,7 @@ following command: ./manage.py makemigrations -This will create a migration file in the `migrations` directory of your custom app. You can then apply the migration +This will create a migration file in the ``migrations`` directory of your custom app. You can then apply the migration by running: .. code:: python @@ -209,8 +209,8 @@ as the built-in fields from any custom code you write, the API, or from the admi Adding ``Extra Fields`` ======================= -If a user does not want to create a custom target model, they can use the `EXTRA_FIELDS` -setting to add extra fields to the `Target` model. This method is simpler and does not require +If a user does not want to create a custom target model, they can use the ``EXTRA_FIELDS`` +setting to add extra fields to the ``Target`` model. This method is simpler and does not require any database migrations, but is less flexible than creating a custom target model. **Note**: There is a performance hit when using extra fields. Try to use @@ -219,7 +219,7 @@ the built in fields whenever possible. Enabling extra fields ~~~~~~~~~~~~~~~~~~~~~ -To start, find the `EXTRA_FIELDS` definition in your ``settings.py``: +To start, find the ``EXTRA_FIELDS`` definition in your ``settings.py``: .. code:: python From ca60ad66620e2c346b16a0b41082a861f678c9d6 Mon Sep 17 00:00:00 2001 From: "William (Lindy) Lindstrom" Date: Wed, 17 Apr 2024 20:57:43 +0100 Subject: [PATCH 37/69] fix that --- docs/targets/target_fields.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index b11c5ad5c..f41a01a2f 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -130,7 +130,7 @@ project, you will need to add the following line: TARGET_MODEL_CLASS = 'custom_code.models.UserDefinedTarget' -Changing ``c`ustom_code`` to the name of your custom app and ``UserDefinedTarget`` to the name of your custom target model. +Changing ``custom_code`` to the name of your custom app and ``UserDefinedTarget`` to the name of your custom target model. Creating Migrations ~~~~~~~~~~~~~~~~~~~~ From 78bda24ce400f1d7ca282738c7bbbd5f85ab071e Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 17 Apr 2024 13:17:49 -0700 Subject: [PATCH 38/69] add line numbers in the explanation --- docs/targets/target_fields.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index f41a01a2f..b190bd1a7 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -116,8 +116,8 @@ The following is an example of a custom target model that adds a boolean field a ('delete_target', 'Delete Target'), ) -The model name, ``UserDefinedTarget`` in the example, can be replaced by whatever CamelCase name you want, but -it must be a subclass of ``tom_targets.BaseTarget``. The permissions in the class Meta are required for the +The model name, ``UserDefinedTarget`` in the example (line 6), can be replaced by whatever CamelCase name you want, but +it must be a subclass of ``tom_targets.BaseTarget``. The permissions in the class Meta (lines 15-20) are required for the TOM Toolkit to work properly. The ``hidden`` attribute can be set to ``True`` to hide the field from the target detail page. From 9e0b46c6d012cd7ce7b95e3eba743dfb4c52d3c5 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 18 Apr 2024 18:30:55 -0700 Subject: [PATCH 39/69] add management command --- tom_base/settings.py | 2 +- tom_setup/templates/tom_setup/settings.tmpl | 4 +- .../commands/converttargetextras.py | 162 ++++++++++++++++++ 3 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 tom_targets/management/commands/converttargetextras.py diff --git a/tom_base/settings.py b/tom_base/settings.py index b9d689787..59325d553 100644 --- a/tom_base/settings.py +++ b/tom_base/settings.py @@ -277,7 +277,7 @@ # {'name': 'redshift', 'type': 'number', 'default': 0}, # {'name': 'discoverer', 'type': 'string'}, # {'name': 'eligible', 'type': 'boolean', 'hidden': True}, -# {'name': 'dicovery_date', 'type': 'datetime'} +# {'name': 'discovery_date', 'type': 'datetime'} # ] EXTRA_FIELDS = [] diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index 20073e021..864e28ede 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -286,9 +286,9 @@ HARVESTERS = { # For example: # EXTRA_FIELDS = [ # {'name': 'redshift', 'type': 'number'}, -# {'name': 'discoverer', 'type': 'string'} +# {'name': 'discoverer', 'type': 'string'}, # {'name': 'eligible', 'type': 'boolean'}, -# {'name': 'dicovery_date', 'type': 'datetime'} +# {'name': 'discovery_date', 'type': 'datetime'} # ] EXTRA_FIELDS = [] diff --git a/tom_targets/management/commands/converttargetextras.py b/tom_targets/management/commands/converttargetextras.py new file mode 100644 index 000000000..113554b11 --- /dev/null +++ b/tom_targets/management/commands/converttargetextras.py @@ -0,0 +1,162 @@ +from django.core.management.base import BaseCommand +from django.conf import settings + +from tom_targets.base_models import BaseTarget +from tom_targets.models import Target, TargetExtra + + +class Command(BaseCommand): + """ + This command converts a given TargetExtra into a model field in the current Target model. + This requires a model field to already exist in your UserDefinedTarget model for each Extra Field you wish to + convert. If you have not created a UserDefinedTarget model, you should follow the example given in the + documentation: https://tom-toolkit.readthedocs.io/en/stable/targets/target_fields.html#extending-the-target-model + + Example: + ./manage.py converttargetextras --target_extra redshift discovery_date --model_field redshift discovery_date + + """ + + help = 'A Helper command to convert target extras into UserDefinedTarget Fields' + + def add_arguments(self, parser): + parser.add_argument( + '--target_extra', + nargs='+', + help='TargetExtra to convert into a model field. Accepts multiple TargetExtras. ' + '(Leave blank for interactive.)' + ) + parser.add_argument( + '--model_field', + nargs='+', + default=[], # Default to empty list to allow for interactive mode + help='Model Fields for UserDefinedTarget to accept TargetExtra. Accepts multiple Model Fields. ' + 'Order must match --target_extra order for multiple entries. ' + '(Leave blank for interactive.)' + ) + parser.add_argument( + '--confirm', + action='store_true', + help='Confirm each Target Extra -> Model Field conversion first.', + ) + + def prompt_extra_field(self, extra_field_keys): + """ + Interactive Mode -- Prompt the user to choose a TargetExtra to convert + extra_field_keys: List of valid TargetExtra keys from settings.py + """ + prompt = f'Which Extra Field would you like to convert?\n{self.style.WARNING(extra_field_keys)}\n' + while True: + chosen_extra = input(prompt) + if chosen_extra in extra_field_keys: + break + else: + self.stdout.write(self.style.ERROR("I don't recognize that field. " + "Please choose from the list.")) + return chosen_extra + + def prompt_model_field(self, model_field_keys, chosen_extra): + """ + Interactive Mode -- Prompt the user to choose a Model Field to convert the TargetExtra into + model_field_keys: list of valid fields available for the Target Model + chosen_extra: key for the selected TargetExtra + """ + prompt = f'What is the name of the model field you would like to convert {self.style.SUCCESS(chosen_extra)}' \ + f' into? (Leave blank to skip)\n{self.style.WARNING(model_field_keys)}\n' + while True: + chosen_model_field = input(prompt) + if chosen_model_field in model_field_keys: + break + elif not chosen_model_field: + self.stdout.write(f'Skipping TargetExtra: {self.style.SUCCESS(chosen_extra)}.') + return None + else: + self.stdout.write(self.style.ERROR("I don't recognize that field. " + "Please choose from the list.")) + return chosen_model_field + + def confirm_conversion(self, chosen_extra, chosen_model_field): + """ + Interactive Mode -- Ask for confirmation before converting a Target Extra + """ + prompt = (f'Are you sure that you want to convert the TargetExtra:{self.style.SUCCESS(chosen_extra)} to ' + f'the {Target.__name__} model field:{self.style.SUCCESS(chosen_model_field)} for all Targets?\n' + f' {self.style.WARNING("(y/N)")}\n') + while True: + response = input(prompt).lower() + if not response or response == 'n' or response == 'no': + self.stdout.write(f'Skipping TargetExtra: {self.style.SUCCESS(chosen_extra)}.') + return False + elif response == 'y' or response == 'yes': + return True + else: + self.stdout.write('Invalid response. Please try again.') + + def convert_target_extra(self, chosen_extra, chosen_model_field): + """ + Perform the actual conversion from a `chosen_extra` to a `chosen_model_field` for each target that has one of + these TargetExtras. + + chosen_extra: key for the selected TargetExtra. + chosen_model_field: name of the selected Target field. + """ + for extra in TargetExtra.objects.filter(key=chosen_extra): + target = Target.objects.get(pk=extra.target.pk) + if getattr(target, chosen_model_field, None): + self.stdout.write(f"{self.style.ERROR('Warning:')} {target}.{chosen_model_field} " + f"already has a value: {getattr(target, chosen_model_field)}. Skipping.") + continue + self.stdout.write(f"Setting {Target.__name__}.{chosen_model_field} to {extra.value} for " + f"{target}.") + setattr(target, chosen_model_field, extra.value) + target.save() + extra.delete() + else: + self.stdout.write(f"{self.style.ERROR('Warning:')} No TargetExtra found for " + f"{self.style.SUCCESS(chosen_extra)}.") + + def handle(self, *args, **options): + chosen_extras = options['target_extra'] + chosen_model_fields = options['model_field'] + + # Get all the extra field keys + extra_field_keys = [field['name'] for field in settings.EXTRA_FIELDS] + + # Get all the new model fields + target_model = Target + model_field_keys = [field.name for field in target_model._meta.get_fields() + if field not in BaseTarget._meta.get_fields() and field.name != 'basetarget_ptr'] + + # If no Target Extras were provided, prompt user + if not chosen_extras: + chosen_extras = [self.prompt_extra_field(extra_field_keys)] + + for i, chosen_extra in enumerate(chosen_extras): + # Check that inputs are valid. + if chosen_extra not in extra_field_keys: + self.stdout.write(self.style.ERROR(f"Skipping {chosen_extra} since it is not a valid TargetExtra.")) + continue + try: + chosen_model_field = chosen_model_fields[i] + except IndexError: + # If no Model Field was provided, prompt user + chosen_model_field = self.prompt_model_field(model_field_keys, chosen_extra) + if not chosen_model_field: + continue + if chosen_extra not in extra_field_keys: + self.stdout.write(f'{self.style.ERROR("Warning:")} Skipping {chosen_extra} since it is not a valid' + f' TargetExtra.') + continue + if chosen_model_field not in model_field_keys: + self.stdout.write(f'{self.style.ERROR("Warning:")} Skipping {chosen_model_field} since it is not a ' + f'valid target field for {Target.__name__}.') + continue + + if options['confirm']: + confirmed = self.confirm_conversion(chosen_extra, chosen_model_field) + if not confirmed: + continue + + self.convert_target_extra(chosen_extra, chosen_model_field) + + return From 3eec81b29b2eb3c01a3109b3b5545d5753924d70 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 18 Apr 2024 18:49:33 -0700 Subject: [PATCH 40/69] add docs --- docs/targets/target_fields.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index b190bd1a7..2c69c0b83 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -207,6 +207,30 @@ the target detail page. Any fields added in this way are fully accessible in the TOM Toolkit as ``Target``, and can be used in the same way as the built-in fields from any custom code you write, the API, or from the admin interface. + +Transferring existing ``Extra Field`` Data to you ``Target`` Fields +=================================================================== + +If you have been using ``Extra Fields`` and have now created a custom target model, you may want to transfer the data +from the ``Extra Fields`` to the new fields in your custom target model. This can be done by running a management +command called ``converttargetextras``. To use this command, be sure to have already created your custom target model. +You can run the command as is for an interactive walkthrough. + +.. code:: python + + ./manage.py converttargetextras + +Alternatively, you can run the command with the ``--target_extra`` and/or ``--model_field`` flags to specify one or +more the of the ``Extra Field`` and ``Target Field`` names respectively. + +.. code:: python + + ./manage.py converttargetextras --target_extra extra_bool extra_number --model_field example_bool example_number + +This command will go through each target and transfer the data from the ``Extra Field`` to the ``Target Field``. If the +``Target Field`` is already populated, the data will not be transferred. When finished, the ``Extra Field`` data will be +deleted, and you will likely want to remove the ``EXTRA_FIELDS`` setting from your ``settings.py`` file. + Adding ``Extra Fields`` ======================= If a user does not want to create a custom target model, they can use the ``EXTRA_FIELDS`` From d3a7c6b9d964f4b5e6f38fc63bb4746493efe163 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 18 Apr 2024 22:18:57 -0700 Subject: [PATCH 41/69] remove extra else --- tom_targets/management/commands/converttargetextras.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tom_targets/management/commands/converttargetextras.py b/tom_targets/management/commands/converttargetextras.py index 113554b11..635b91e7b 100644 --- a/tom_targets/management/commands/converttargetextras.py +++ b/tom_targets/management/commands/converttargetextras.py @@ -111,9 +111,7 @@ def convert_target_extra(self, chosen_extra, chosen_model_field): setattr(target, chosen_model_field, extra.value) target.save() extra.delete() - else: - self.stdout.write(f"{self.style.ERROR('Warning:')} No TargetExtra found for " - f"{self.style.SUCCESS(chosen_extra)}.") + def handle(self, *args, **options): chosen_extras = options['target_extra'] From a7bab6da2324d664cccbbed02260967924a3f3cc Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 18 Apr 2024 23:10:52 -0700 Subject: [PATCH 42/69] remove blank lines --- tom_targets/management/commands/converttargetextras.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tom_targets/management/commands/converttargetextras.py b/tom_targets/management/commands/converttargetextras.py index 635b91e7b..a97deae00 100644 --- a/tom_targets/management/commands/converttargetextras.py +++ b/tom_targets/management/commands/converttargetextras.py @@ -112,7 +112,6 @@ def convert_target_extra(self, chosen_extra, chosen_model_field): target.save() extra.delete() - def handle(self, *args, **options): chosen_extras = options['target_extra'] chosen_model_fields = options['model_field'] From c08a111f83e87872f0c285f18326f1c5dc02ccbc Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 10:02:13 -0700 Subject: [PATCH 43/69] fix bug for creating new targets with TargetExtras --- tom_targets/base_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 76e7e8ba3..3ac8b8161 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -302,7 +302,8 @@ def save(self, *args, **kwargs): if created: for extra_field in settings.EXTRA_FIELDS: if extra_field.get('default') is not None: - self.targetextra_set(target=self, key=extra_field['name'], value=extra_field.get('default')).save() + self.targetextra_set.get_or_create(target=self, key=extra_field['name'], + value=extra_field.get('default')) for k, v in extras.items(): target_extra, _ = self.targetextra_set.get_or_create(target=self, key=k) From d6fd7b752d85a1f57cfa45072618cba2dc077ee4 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 10:06:40 -0700 Subject: [PATCH 44/69] fix some spacing to make model.tmpl uncomment better --- tom_setup/templates/tom_setup/models.tmpl | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tom_setup/templates/tom_setup/models.tmpl b/tom_setup/templates/tom_setup/models.tmpl index ba22a8da6..e970a64b3 100644 --- a/tom_setup/templates/tom_setup/models.tmpl +++ b/tom_setup/templates/tom_setup/models.tmpl @@ -1,18 +1,18 @@ -from django.db import models - -from tom_targets.base_models import BaseTarget - - -#class UserDefinedTarget(BaseTarget): -# """ -# A target with fields defined by a user. -# """ -# -# class Meta: -# verbose_name = "target" -# permissions = ( -# ('view_target', 'View Target'), -# ('add_target', 'Add Target'), -# ('change_target', 'Change Target'), -# ('delete_target', 'Delete Target'), -# ) +# from django.db import models +# +# from tom_targets.base_models import BaseTarget +# +# +# class UserDefinedTarget(BaseTarget): +# """ +# A target with fields defined by a user. +# """ +# +# class Meta: +# verbose_name = "target" +# permissions = ( +# ('view_target', 'View Target'), +# ('add_target', 'Add Target'), +# ('change_target', 'Change Target'), +# ('delete_target', 'Delete Target'), +# ) From 8d6f0cd4cb66f3c09792dfcf7c245ddf8d4487d8 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 11:24:46 -0700 Subject: [PATCH 45/69] add list --- tom_targets/management/commands/converttargetextras.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tom_targets/management/commands/converttargetextras.py b/tom_targets/management/commands/converttargetextras.py index a97deae00..aeedffddc 100644 --- a/tom_targets/management/commands/converttargetextras.py +++ b/tom_targets/management/commands/converttargetextras.py @@ -39,6 +39,11 @@ def add_arguments(self, parser): action='store_true', help='Confirm each Target Extra -> Model Field conversion first.', ) + parser.add_argument( + '--list', + action='store_true', + help='Provide a list of available TargetExtras and Model Fields.', + ) def prompt_extra_field(self, extra_field_keys): """ @@ -124,6 +129,11 @@ def handle(self, *args, **options): model_field_keys = [field.name for field in target_model._meta.get_fields() if field not in BaseTarget._meta.get_fields() and field.name != 'basetarget_ptr'] + if options['list']: + self.stdout.write(f'Available TargetExtras: {self.style.WARNING(extra_field_keys)}') + self.stdout.write(f'Available Model Fields: {self.style.WARNING(model_field_keys)}') + return + # If no Target Extras were provided, prompt user if not chosen_extras: chosen_extras = [self.prompt_extra_field(extra_field_keys)] From 2fa46625cc71538c979904d0a25a86ce1319668c Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 11:30:32 -0700 Subject: [PATCH 46/69] create failing test that should be fixed with management command merge --- tom_targets/tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index 4813fc11d..d3345d20c 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -291,7 +291,7 @@ def test_create_target_with_tags(self): self.assertTrue(target.targetextra_set.filter(key='category', value='type2').exists()) @override_settings(EXTRA_FIELDS=[ - {'name': 'wins', 'type': 'number'}, + {'name': 'wins', 'type': 'number', 'default': '12'}, {'name': 'checked', 'type': 'boolean'}, {'name': 'birthdate', 'type': 'datetime'}, {'name': 'author', 'type': 'string'} From 201d94a70137b94172f2d14f3d80127af17611ab Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 15:25:58 -0700 Subject: [PATCH 47/69] doc updates --- docs/targets/target_fields.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index 2c69c0b83..b0fca205a 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -208,13 +208,13 @@ Any fields added in this way are fully accessible in the TOM Toolkit as ``Target as the built-in fields from any custom code you write, the API, or from the admin interface. -Transferring existing ``Extra Field`` Data to you ``Target`` Fields +Transferring existing ``Extra Field`` Data to your ``Target`` Fields =================================================================== If you have been using ``Extra Fields`` and have now created a custom target model, you may want to transfer the data from the ``Extra Fields`` to the new fields in your custom target model. This can be done by running a management command called ``converttargetextras``. To use this command, be sure to have already created your custom target model. -You can run the command as is for an interactive walkthrough. +You can run the command without arguments for an interactive walkthrough. .. code:: python From efaa8295fb2779112f36813c6ec332a2323d1b68 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 16:10:44 -0700 Subject: [PATCH 48/69] update all values for a target extra id main value updated. --- tom_targets/forms.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tom_targets/forms.py b/tom_targets/forms.py index 965671299..45e8c9cbb 100644 --- a/tom_targets/forms.py +++ b/tom_targets/forms.py @@ -72,11 +72,12 @@ def save(self, commit=True): if commit: for field in settings.EXTRA_FIELDS: if self.cleaned_data.get(field['name']) is not None: - TargetExtra.objects.update_or_create( - target=instance, - key=field['name'], - defaults={'value': self.cleaned_data[field['name']]} + updated_target_extra, _ = TargetExtra.objects.update_or_create( + target=instance, + key=field['name'], + defaults={'value': self.cleaned_data[field['name']]} ) + updated_target_extra.save() # Save groups for this target for group in self.cleaned_data['groups']: assign_perm('tom_targets.view_target', group, instance) From bab31496ae229380fc293c4e5c344a971ab043b1 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 19 Apr 2024 16:41:18 -0700 Subject: [PATCH 49/69] update poetry.lock --- poetry.lock | 2666 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2666 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..7135afd01 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2666 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +optional = false +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "asdf" +version = "2.15.0" +description = "Python implementation of the ASDF Standard" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf-2.15.0-py3-none-any.whl", hash = "sha256:11fae326d1f3a39a2c0c4cd42d4fcb789ef46f9ebe00c79564f34b066f05027a"}, + {file = "asdf-2.15.0.tar.gz", hash = "sha256:686f1c91ebf987d41f915cfb6aa70940d7ad17f87ede0be70463147ad2314587"}, +] + +[package.dependencies] +asdf-standard = ">=1.0.1" +asdf-transform-schemas = ">=0.3" +asdf-unit-schemas = ">=0.1" +importlib-metadata = ">=4.11.4" +importlib-resources = {version = ">=3", markers = "python_version < \"3.9\""} +jmespath = ">=0.6.2" +jsonschema = ">=4.0.1,<4.18" +numpy = [ + {version = ">=1.20", markers = "python_version >= \"3.9\""}, + {version = ">=1.20,<1.25", markers = "python_version < \"3.9\""}, +] +packaging = ">=19" +pyyaml = ">=5.4.1" +semantic-version = ">=2.8" + +[package.extras] +all = ["lz4 (>=0.10)"] +docs = ["sphinx-asdf (>=0.1.4)", "tomli"] +tests = ["astropy (>=5.0.4)", "fsspec[http] (>=2022.8.2)", "gwcs (>=0.18.3)", "lz4 (>=0.10)", "psutil", "pytest (>=6)", "pytest-doctestplus", "pytest-openfiles", "pytest-remotedata"] + +[[package]] +name = "asdf-astropy" +version = "0.4.0" +description = "ASDF serialization support for astropy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf-astropy-0.4.0.tar.gz", hash = "sha256:462b674601fe2c4a2f45c92bed3e21effd0d58addc7ec4335c545118bc582eb6"}, + {file = "asdf_astropy-0.4.0-py3-none-any.whl", hash = "sha256:561d47978e7012cd0488ff3386fea76102048cc8d3e75e470c1a39971eef86af"}, +] + +[package.dependencies] +asdf = ">=2.13" +asdf-coordinates-schemas = ">=0.1" +asdf-transform-schemas = ">=0.2.2" +astropy = ">=5.0.4" +importlib-resources = {version = ">=3", markers = "python_version < \"3.9\""} +numpy = ">=1.20" +packaging = ">=19" + +[package.extras] +docs = ["docutils", "graphviz", "matplotlib", "sphinx", "sphinx-asdf", "sphinx-astropy", "sphinx-automodapi", "tomli"] +test = ["coverage", "pytest-astropy", "scipy"] + +[[package]] +name = "asdf-coordinates-schemas" +version = "0.2.0" +description = "ASDF schemas for coordinates" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf_coordinates_schemas-0.2.0-py3-none-any.whl", hash = "sha256:5450a70f0b548cd7d90599d0b2b98285219f2376431809373eb461ce7192f207"}, + {file = "asdf_coordinates_schemas-0.2.0.tar.gz", hash = "sha256:e3f9a50872e13749a7eec2dc3ccb4af93280f5e5e20a8d70a3d83073de8dd5f4"}, +] + +[package.dependencies] +asdf = ">=2.12.1" + +[package.extras] +docs = ["astropy (>=5.0.4)", "docutils", "graphviz", "matplotlib", "sphinx", "sphinx-asdf (>=0.1.3)", "sphinx-astropy", "sphinx-rtd-theme", "tomli"] +test = ["pytest"] + +[[package]] +name = "asdf-standard" +version = "1.0.3" +description = "The ASDF Standard schemas" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf_standard-1.0.3-py3-none-any.whl", hash = "sha256:1c628379c75f0663b6376a7e681d31b1b54391053e53447c9921fb04c26d41da"}, + {file = "asdf_standard-1.0.3.tar.gz", hash = "sha256:afd8ff9a70e7b17f6bcc64eb92a544867d5d4fe1f0076719142fdf62b96cfd44"}, +] + +[package.dependencies] +importlib-resources = {version = ">=3", markers = "python_version < \"3.9\""} + +[package.extras] +docs = ["docutils", "graphviz", "matplotlib", "sphinx", "sphinx-asdf (>=0.1.3)", "sphinx-astropy", "sphinx-rtd-theme", "toml"] +test = ["asdf (>=2.8.0)", "astropy (>=5.0.4)", "gwcs", "packaging (>=16.0)", "pytest", "pyyaml"] + +[[package]] +name = "asdf-transform-schemas" +version = "0.4.0" +description = "ASDF schemas for transforms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf_transform_schemas-0.4.0-py3-none-any.whl", hash = "sha256:fed6cab061ee759c97a30bfe4ec06fdea7af72268b355301082e97994e4313e8"}, + {file = "asdf_transform_schemas-0.4.0.tar.gz", hash = "sha256:de7fdc3fee35fb957fc32957877a0e9d7dd4d2e851bd631a7259f11c2bd294ca"}, +] + +[package.dependencies] +asdf-standard = ">=1.0.1" +importlib-resources = {version = ">=3", markers = "python_version < \"3.9\""} + +[package.extras] +docs = ["astropy (>=5.0.4)", "docutils", "graphviz", "matplotlib", "sphinx", "sphinx-asdf (>=0.1.3)", "sphinx-astropy", "sphinx-rtd-theme", "tomli"] +test = ["asdf (>=2.8.0)", "asdf-astropy", "pytest", "scipy"] + +[[package]] +name = "asdf-unit-schemas" +version = "0.2.0" +description = "DEPRECATED: ASDF schemas for units" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf_unit_schemas-0.2.0-py3-none-any.whl", hash = "sha256:6a2af1aeda688bc4994e7c933badbee0ff7e61f1ba3121159635c51510979b8d"}, + {file = "asdf_unit_schemas-0.2.0.tar.gz", hash = "sha256:d995c45b5531ef1fe2e0525db30c7fc8b36df9447116ee067ce4461eea7e4440"}, +] + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme", "tomli"] +test = ["asdf", "pytest"] + +[[package]] +name = "asdf-wcs-schemas" +version = "0.3.0" +description = "ASDF WCS schemas" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asdf_wcs_schemas-0.3.0-py3-none-any.whl", hash = "sha256:ee0fc4e8894c6b09c9cea975a6831e9c8feac7678fe551d46a85c06d6474f493"}, + {file = "asdf_wcs_schemas-0.3.0.tar.gz", hash = "sha256:51e593bc3c2941bed381409f84b1a8cb60a003548e3b8b8caf06f337b5d034e5"}, +] + +[package.dependencies] +asdf-standard = ">=1.0.1" +asdf-transform-schemas = ">=0.3.0" +asdf-unit-schemas = ">=0.1.0" +importlib-resources = {version = ">=3", markers = "python_version < \"3.9\""} + +[package.extras] +docs = ["astropy (>=5.0.4)", "docutils", "graphviz", "matplotlib", "sphinx", "sphinx-asdf (>=0.1.3)", "sphinx-astropy", "sphinx-rtd-theme", "tomli"] +test = ["asdf (>=2.8.0)", "asdf-astropy", "pytest (>=4.6.0)", "pytest-openfiles (>=0.5.0)"] + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "astroplan" +version = "0.10" +description = "Observation planning package for astronomers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "astroplan-0.10.tar.gz", hash = "sha256:1670c9c42143d62924e8a6e0287ec25935fcf985a8b928438bc4da0241f4875a"}, +] + +[package.dependencies] +astropy = ">=4" +numpy = "<2" +pytz = "*" +six = "*" + +[package.extras] +all = ["astroquery", "matplotlib (>=1.4)"] +docs = ["astroquery", "matplotlib (>=1.4)", "sphinx-astropy[confv2]", "sphinx-rtd-theme"] +plotting = ["astroquery", "matplotlib (>=1.4)"] +test = ["pytest-astropy", "pytest-mpl"] + +[[package]] +name = "astropy" +version = "5.2.2" +description = "Astronomy and astrophysics core library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "astropy-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66522e897daf3766775c00ef5c63b69beb0eb359e1f45d18745d0f0ca7f29cc1"}, + {file = "astropy-5.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ccf6f16cf7e520247ecc9d1a66dd4c3927fd60622203bdd1d06655ad81fa18f"}, + {file = "astropy-5.2.2-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3d0c37da922cdcb81e74437118fabd64171cbfefa06c7ea697a270e82a8164f2"}, + {file = "astropy-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04464e664a22382626ce9750ebe943b80a718dc8347134b9d138b63a2029f67a"}, + {file = "astropy-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f60cea0fa7cb6ebbd90373e48c07f5d459e95dfd6363f50e316e2db7755bead"}, + {file = "astropy-5.2.2-cp310-cp310-win32.whl", hash = "sha256:6c3abb2fa8ebaaad77875a02e664c1011f35bd0c0ef7d35a39b03c859de1129a"}, + {file = "astropy-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:185ade8c33cea34ba791b282e937686d98b4e205d4f343e686a4666efab2f6e7"}, + {file = "astropy-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f61c612e90e3dd3c075e99a61dedd53331c4577016c1d571aab00b95ca1731ab"}, + {file = "astropy-5.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3881e933ea870a27e5d6896443401fbf51e3b7e57c6356f333553f5ff0070c72"}, + {file = "astropy-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f210b5b4062030388437b9aca4bbf68f9063b2b27184006814a09fab41ac270e"}, + {file = "astropy-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e14b5a22f24ae5cf0404f21a4de135e26ca3c9cf55aefc5b0264a9ce24b53b0b"}, + {file = "astropy-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6768b3a670cdfff6c2416b3d7d1e4231839608299b32367e8b095959fc6733a6"}, + {file = "astropy-5.2.2-cp311-cp311-win32.whl", hash = "sha256:0aad85604cad40189b13d66bb46fb2a95df1a9095992071b31c3fa35b476fdbc"}, + {file = "astropy-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:ac944158794a88789a007892ad91db35da14f689da1ab37c33c8de770a27f717"}, + {file = "astropy-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6703860deecd384bba2d2e338f77a0e7b46672812d27ed15f95e8faaa89fcd35"}, + {file = "astropy-5.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:124ef2a9f9b1cdbc1a5d514f7e57538253bb67ad031215f5f5405fc4cd31a4cd"}, + {file = "astropy-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:800501cc626aef0780dfb66156619699e98cb48854ed710f1ae3708aaab79f6e"}, + {file = "astropy-5.2.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22396592aa9b1653d37d552d3c52a8bb27ef072d077fad43b64faf841b1dcbf3"}, + {file = "astropy-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093782b1f0177c3dd2c04181ec016d8e569bd9e862b48236e40b14e2a7399170"}, + {file = "astropy-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0c664f9194a4a3cece6215f651a9bc22c3cbd1f52dd450bd4d94eaf36f13c06c"}, + {file = "astropy-5.2.2-cp38-cp38-win32.whl", hash = "sha256:35ce00bb3dbc8bf7c842a0635354a5023cb64ae9c1925aa9b54629cf7fed2abe"}, + {file = "astropy-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:8304b590b20f9c161db85d5eb65d4c6323b3370a17c96ae163b18a0071cbd68a"}, + {file = "astropy-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:855748c2f1aedee5d770dfec8334109f1bcd1c1cee97f5915d3e888f43c04acf"}, + {file = "astropy-5.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ef9acc55c5fd70c7c78370389e79fb044321e531ac1facb7bddeef89d3132e3"}, + {file = "astropy-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f30b5d153b9d119783b96b948a3e0c4eb668820c06d2e8ba72f6ea989e4af5c1"}, + {file = "astropy-5.2.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:530e6911a54a42e9f15b1a75dc3c699be3946c0b6ffdcfdcf4e14ae5fcfcd236"}, + {file = "astropy-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae3b383ac84fe6765e275f897f4010cc6afe6933607b7468561414dffdc4d915"}, + {file = "astropy-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b00a4cd49f8264a338b0020717bff104fbcca800bd50bf0a415d952078258a39"}, + {file = "astropy-5.2.2-cp39-cp39-win32.whl", hash = "sha256:b7167b9965ebd78b7c9da7e98a943381b25e23d041bd304ec2e35e8ec811cefc"}, + {file = "astropy-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:df81b8f23c5e906d799b47d2d8462707c745df38cafae0cd6674ef09e9a41789"}, + {file = "astropy-5.2.2.tar.gz", hash = "sha256:e6a9e34716bda5945788353c63f0644721ee7e5447d16b1cdcb58c48a96b0d9c"}, +] + +[package.dependencies] +numpy = ">=1.20" +packaging = ">=19.0" +pyerfa = ">=2.0" +PyYAML = ">=3.13" + +[package.extras] +all = ["asdf (>=2.10.0)", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2022.8.2)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "matplotlib (>=3.1,!=3.4.0,!=3.5.2)", "mpmath", "pandas", "pyarrow (>=5.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2022.8.2)", "scipy (>=1.5)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] +docs = ["Jinja2 (>=3.0)", "matplotlib (>=3.1,!=3.4.0,!=3.5.2)", "pytest (>=7.0)", "scipy (>=1.3)", "sphinx", "sphinx-astropy (>=1.6)", "sphinx-changelog (>=1.2.0)"] +recommended = ["matplotlib (>=3.1,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] +test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist"] +test-all = ["coverage[toml]", "ipython (>=4.2)", "objgraph", "pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "sgp4 (>=2.3)", "skyfield (>=1.20)"] + +[[package]] +name = "astropy" +version = "5.3.4" +description = "Astronomy and astrophysics core library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "astropy-5.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6c63abc95d094cd3062e32c1ebf80c07502e4f3094b1e276458db5ce6b6a2"}, + {file = "astropy-5.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e85871ec762fc7eab2f7e716c97dad1b3c546bb75941ea7fae6c8eadd51f0bf8"}, + {file = "astropy-5.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e82fdad3417b70af381945aa42fdae0f11bc9aaf94b95027b1e24379bf847d6"}, + {file = "astropy-5.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbce56f46ec1051fd67a5e2244e5f2e08599a176fe524c0bee2294c62be317b3"}, + {file = "astropy-5.3.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a489c2322136b76a43208e3e9b5a7947a7fd624a10e49d2909b94f12b624da06"}, + {file = "astropy-5.3.4-cp310-cp310-win32.whl", hash = "sha256:c713695e39f5a874705bc3bd262c5d218890e3e7c43f0b6c0b5e7d46bdff527c"}, + {file = "astropy-5.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:2576579befb0674cdfd18f5cc138c919a109c6886a25aa3d8ed8ab4e4607c581"}, + {file = "astropy-5.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ce096dde6b86a87aa84aec4198732ec379fbb7649af66a96f85b96d17214c2a"}, + {file = "astropy-5.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:830fb4b19c36bf8092fdd74ecf9df5b78c6435bf571c5e09b7f644875148a058"}, + {file = "astropy-5.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a707c534408d26d90014a1938af883f6cbf43a3dd78df8bb9a191d275c09f8d"}, + {file = "astropy-5.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0bb2b9b93bc879bcd032931e7fc07c3a3de6f9546fed17f0f12974e0ffc83e0"}, + {file = "astropy-5.3.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1fa4437fe8d1e103f14cb1cb4e8449c93ae4190b5e9fd97e9c61a5155de9af0d"}, + {file = "astropy-5.3.4-cp311-cp311-win32.whl", hash = "sha256:c656c7fd3d862bcb9d3c4a87b8e9488d0c351b4edf348410c09a26641b9d4731"}, + {file = "astropy-5.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:4c4971abae8e3ddfb8f40447d78aaf24e6ce44b976b3874770ff533609050366"}, + {file = "astropy-5.3.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:887db411555692fb1858ae305f87fd2ff42a021b68c78abbf3fa1fc64641e895"}, + {file = "astropy-5.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e4033d7a6bd2da38b83ec65f7282dfeb2641f2b2d41b1cd392cdbe3d6f8abfff"}, + {file = "astropy-5.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2cc6503b79d4fb61ca80e1d37dd609fabca6d2e0124e17f831cc08c2e6ff75e"}, + {file = "astropy-5.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3f9fe1d76d151428a8d2bc7d50f4a47ae6e7141c11880a3ad259ac7b906b03"}, + {file = "astropy-5.3.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6e0f7ecbb2a8acb3eace99bcaca30dd1ce001e6f4750a009fd9cc3b8d1b49c58"}, + {file = "astropy-5.3.4-cp312-cp312-win32.whl", hash = "sha256:d915e6370315a1a6a40c2576e77d0063f48cc3b5f8873087cad8ad19dd429d19"}, + {file = "astropy-5.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:69f5a3789a8a4cb00815630b63f950be629a983896dc1aba92566ccc7937a77d"}, + {file = "astropy-5.3.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5d1a1be788344f11a94a5356c1a25b4d45f1736b740edb4d8e3a272b872a8fa"}, + {file = "astropy-5.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae59e4d41461ad96a2573bc51408000a7b4f90dce2bad07646fa6409a12a5a74"}, + {file = "astropy-5.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4c4d3a14e8e3a33208683331b16a721ab9f9493ed998d34533532fdaeaa3642"}, + {file = "astropy-5.3.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f58f53294f07cd3f9173bb113ad60d2cd823501c99251891936202fed76681"}, + {file = "astropy-5.3.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f79400dc6641bb0202a8998cfb08ad1afe197818e27c946491a292e2ffd16a1b"}, + {file = "astropy-5.3.4-cp39-cp39-win32.whl", hash = "sha256:fd0baa7621d03aa74bb8ba673d7955381d15aed4f30dc2a56654560401fc3aca"}, + {file = "astropy-5.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ed6116d07de02183d966e9a5dabc86f6fd3d86cc3e1e8b9feef89fd757be8a6"}, + {file = "astropy-5.3.4.tar.gz", hash = "sha256:d490f7e2faac2ccc01c9244202d629154259af8a979104ced89dc4ace4e6f1d8"}, +] + +[package.dependencies] +numpy = ">=1.21,<2" +packaging = ">=19.0" +pyerfa = ">=2.0" +PyYAML = ">=3.13" + +[package.extras] +all = ["asdf (>=2.10.0)", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2022.8.2)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "mpmath", "pandas", "pre-commit", "pyarrow (>=5.0.0)", "pytest (>=7.0,<8)", "pytz", "s3fs (>=2022.8.2)", "scipy (>=1.5)", "sortedcontainers", "typing-extensions (>=3.10.0.1)"] +docs = ["Jinja2 (>=3.0)", "matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "pytest (>=7.0,<8)", "scipy (>=1.3)", "sphinx", "sphinx-astropy (>=1.6)", "sphinx-changelog (>=1.2.0)"] +recommended = ["matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"] +test = ["pytest (>=7.0,<8)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist"] +test-all = ["coverage[toml]", "ipython (>=4.2)", "objgraph", "pytest (>=7.0,<8)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "sgp4 (>=2.3)", "skyfield (>=1.20)"] + +[[package]] +name = "astroquery" +version = "0.4.7" +description = "Functions and classes to access online astronomical data resources" +optional = false +python-versions = ">=3.7" +files = [ + {file = "astroquery-0.4.7-py3-none-any.whl", hash = "sha256:dfa8ca46ca0a983f66e9547c774601b331770242f9f6245df5e603f795d75540"}, + {file = "astroquery-0.4.7.tar.gz", hash = "sha256:047fbacb0a4faec4cdb62675e919c244c1c35e661044fcbb6c9a933331747ec9"}, +] + +[package.dependencies] +astropy = ">=4.2.1" +beautifulsoup4 = ">=4.8" +html5lib = ">=0.999" +keyring = ">=15.0" +numpy = ">=1.18" +pyvo = ">=1.1" +requests = ">=2.19" + +[package.extras] +all = ["astropy-healpix", "boto3", "mocpy (>=0.9)", "regions"] +docs = ["fsspec[http]", "matplotlib", "scipy", "sphinx-astropy (>=1.5)"] +test = ["matplotlib", "pytest-astropy", "pytest-dependency", "pytest-rerunfailures"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.dependencies] +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "backports-tarfile" +version = "1.1.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.1.0-py3-none-any.whl", hash = "sha256:b2f4df351db942d094db94588bbf2c6938697a5f190f44c934acc697da56008b"}, + {file = "backports_tarfile-1.1.0.tar.gz", hash = "sha256:91d59138ea401ee2a95e8b839c1e2f51f3e9ca76bdba8b6a29f8d773564686a8"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + +[[package]] +name = "backports-zoneinfo" +version = "0.2.1" +description = "Backport of the standard library zoneinfo module" +optional = false +python-versions = ">=3.6" +files = [ + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, + {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, + {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, + {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, + {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, +] + +[package.extras] +tzdata = ["tzdata"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +optional = false +python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "coveralls" +version = "3.3.1" +description = "Show coverage stats online via coveralls.io" +optional = false +python-versions = ">= 3.5" +files = [ + {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"}, + {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, +] + +[package.dependencies] +coverage = ">=4.1,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<7.0" +docopt = ">=0.6.1" +requests = ">=1.0.0" + +[package.extras] +yaml = ["PyYAML (>=3.10)"] + +[[package]] +name = "crispy-bootstrap4" +version = "2024.1" +description = "Bootstrap4 template pack for django-crispy-forms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "crispy-bootstrap4-2024.1.tar.gz", hash = "sha256:208673bf6a25892a656971af7a00e18ba2f7f06cd4a0d667923bd6134e64d276"}, + {file = "crispy_bootstrap4-2024.1-py3-none-any.whl", hash = "sha256:46cf98777a28621d240bf71eb36d4a26ff86e9e19be1cfd822645f4043d18c31"}, +] + +[package.dependencies] +django = ">=4.2" +django-crispy-forms = ">=2.0" + +[[package]] +name = "cryptography" +version = "42.0.5" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, + {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, + {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, + {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, + {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, + {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, + {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, + {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, + {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, + {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, + {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, + {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, + {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, + {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "django" +version = "4.2.11" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.11-py3-none-any.whl", hash = "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"}, + {file = "Django-4.2.11.tar.gz", hash = "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-bootstrap4" +version = "24.3" +description = "Django extensions by Zostera" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_bootstrap4-24.3-py3-none-any.whl", hash = "sha256:b555d87740a571036f100ad6026b1f62aabcb913404fb7f08f521881019b14bc"}, + {file = "django_bootstrap4-24.3.tar.gz", hash = "sha256:819bc0ba7b25fcdeb12eb04353962436dbe95b228ba4cf4b49f5d3fee53692e1"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.8.0" +Django = ">=4.1" + +[[package]] +name = "django-contrib-comments" +version = "2.2.0" +description = "The code formerly known as django.contrib.comments." +optional = false +python-versions = "*" +files = [ + {file = "django-contrib-comments-2.2.0.tar.gz", hash = "sha256:48de00f15677e016a216aeff205d6e00e4391c9a5702136c64119c472b7356da"}, + {file = "django_contrib_comments-2.2.0-py3-none-any.whl", hash = "sha256:2ca79060bbc8fc5b636981ef6e50f35ab83649af75fc1be47bf770636be3271c"}, +] + +[package.dependencies] +Django = ">=2.2" + +[[package]] +name = "django-crispy-forms" +version = "2.1" +description = "Best way to have Django DRY forms" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-crispy-forms-2.1.tar.gz", hash = "sha256:4d7ec431933ad4d4b5c5a6de4a584d24613c347db9ac168723c9aaf63af4bb96"}, + {file = "django_crispy_forms-2.1-py3-none-any.whl", hash = "sha256:d592044771412ae1bd539cc377203aa61d4eebe77fcbc07fbc8f12d3746d4f6b"}, +] + +[package.dependencies] +django = ">=4.2" + +[[package]] +name = "django-dramatiq" +version = "0.11.6" +description = "A Django app for Dramatiq." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_dramatiq-0.11.6-py3-none-any.whl", hash = "sha256:9843f5e4d019bf8f8c89e78b39ba037b3552f17f587f49633cc5e13774ac0ab0"}, + {file = "django_dramatiq-0.11.6.tar.gz", hash = "sha256:f8b47a1654c4a8a1a0b4d3215b6b3ec189cb28dd4dec5afb5437ada48ee38ff1"}, +] + +[package.dependencies] +django = ">=3.2" +dramatiq = ">=1.11" + +[package.extras] +dev = ["bumpversion", "flake8", "flake8-quotes", "isort", "pytest", "pytest-cov", "pytest-django", "twine"] + +[[package]] +name = "django-extensions" +version = "3.2.3" +description = "Extensions for Django" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, + {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, +] + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-filter" +version = "24.2" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, + {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, +] + +[package.dependencies] +Django = ">=4.2" + +[[package]] +name = "django-gravatar2" +version = "1.4.4" +description = "Essential Gravatar support for Django. Features helper methods, templatetags and a full test suite!" +optional = false +python-versions = "*" +files = [ + {file = "django-gravatar2-1.4.4.tar.gz", hash = "sha256:c813280967511ced93eea0359f60e5369c35b3311efe565c3e5d4ab35c10c9ee"}, + {file = "django_gravatar2-1.4.4-py2.py3-none-any.whl", hash = "sha256:545a6c2c5c624c7635dec29c7bc0be1a2cb89c9b8821af8616ae9838827cc35b"}, +] + +[[package]] +name = "django-guardian" +version = "2.4.0" +description = "Implementation of per object permissions for Django." +optional = false +python-versions = ">=3.5" +files = [ + {file = "django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0"}, + {file = "django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697"}, +] + +[package.dependencies] +Django = ">=2.2" + +[[package]] +name = "djangorestframework" +version = "3.15.1" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, +] + +[package.dependencies] +"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} +django = ">=3.0" + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] + +[[package]] +name = "dramatiq" +version = "1.16.0" +description = "Background Processing for Python 3." +optional = false +python-versions = ">=3.7" +files = [ + {file = "dramatiq-1.16.0-py3-none-any.whl", hash = "sha256:650860af82a98905ee03f7cc94b7c356f89528e3008c213aee6a35e2faecde05"}, + {file = "dramatiq-1.16.0.tar.gz", hash = "sha256:00a676a96d0f47ea4ba59a82018dd3d8885fb8cec7765dc8209142f4b493870e"}, +] + +[package.dependencies] +prometheus-client = ">=0.2" +redis = {version = ">=2.0,<6.0", optional = true, markers = "extra == \"redis\""} +watchdog = {version = "*", optional = true, markers = "extra == \"watch\""} +watchdog-gevent = {version = "*", optional = true, markers = "extra == \"watch\""} + +[package.extras] +all = ["gevent (>=1.1)", "pika (>=1.0,<2.0)", "pylibmc (>=1.5,<2.0)", "redis (>=2.0,<6.0)", "watchdog", "watchdog-gevent"] +dev = ["alabaster", "bumpversion", "flake8", "flake8-bugbear", "flake8-quotes", "gevent (>=1.1)", "hiredis", "isort", "pika (>=1.0,<2.0)", "pylibmc (>=1.5,<2.0)", "pytest", "pytest-benchmark[histogram]", "pytest-cov", "redis (>=2.0,<6.0)", "sphinx", "sphinxcontrib-napoleon", "tox", "twine", "watchdog", "watchdog-gevent", "wheel"] +gevent = ["gevent (>=1.1)"] +memcached = ["pylibmc (>=1.5,<2.0)"] +rabbitmq = ["pika (>=1.0,<2.0)"] +redis = ["redis (>=2.0,<6.0)"] +watch = ["watchdog", "watchdog-gevent"] + +[[package]] +name = "factory-boy" +version = "3.3.0" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +optional = false +python-versions = ">=3.7" +files = [ + {file = "factory_boy-3.3.0-py2.py3-none-any.whl", hash = "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c"}, + {file = "factory_boy-3.3.0.tar.gz", hash = "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"}, +] + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "sqlalchemy-utils", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + +[[package]] +name = "faker" +version = "24.11.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-24.11.0-py3-none-any.whl", hash = "sha256:adb98e771073a06bdc5d2d6710d8af07ac5da64c8dc2ae3b17bb32319e66fd82"}, + {file = "Faker-24.11.0.tar.gz", hash = "sha256:34b947581c2bced340c39b35f89dbfac4f356932cfff8fe893bde854903f0e6e"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" +typing-extensions = {version = ">=3.10.0.1", markers = "python_version <= \"3.8\""} + +[[package]] +name = "fits2image" +version = "0.4.7" +description = "Common libraries for the conversion and scaling of fits images" +optional = false +python-versions = "*" +files = [ + {file = "fits2image-0.4.7-py3-none-any.whl", hash = "sha256:0727d0dfef482a1493399f3b44b59085fd0beb1b9d81aa32fd1882272561dae5"}, + {file = "fits2image-0.4.7.tar.gz", hash = "sha256:25a29c0b442c4d025d6bf546c677b7539d3a57cedfc65ee87315c618feece387"}, +] + +[package.dependencies] +astropy = "*" +numpy = "*" +Pillow = "*" + +[[package]] +name = "flake8" +version = "7.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, + {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.11.0,<2.12.0" +pyflakes = ">=3.2.0,<3.3.0" + +[[package]] +name = "gevent" +version = "24.2.1" +description = "Coroutine-based network library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "gevent-24.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1df555431f5cd5cc189a6ee3544d24f8c52f2529134685f1e878c4972ab026"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14532a67f7cb29fb055a0e9b39f16b88ed22c66b96641df8c04bdc38c26b9ea5"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd23df885318391856415e20acfd51a985cba6919f0be78ed89f5db9ff3a31cb"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ca80b121bbec76d7794fcb45e65a7eca660a76cc1a104ed439cdbd7df5f0b060"}, + {file = "gevent-24.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9913c45d1be52d7a5db0c63977eebb51f68a2d5e6fd922d1d9b5e5fd758cc98"}, + {file = "gevent-24.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:918cdf8751b24986f915d743225ad6b702f83e1106e08a63b736e3a4c6ead789"}, + {file = "gevent-24.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d5325ccfadfd3dcf72ff88a92fb8fc0b56cacc7225f0f4b6dcf186c1a6eeabc"}, + {file = "gevent-24.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91"}, + {file = "gevent-24.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682"}, + {file = "gevent-24.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d"}, + {file = "gevent-24.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc"}, + {file = "gevent-24.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9"}, + {file = "gevent-24.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f"}, + {file = "gevent-24.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388"}, + {file = "gevent-24.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5"}, + {file = "gevent-24.2.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:8f4b8e777d39013595a7740b4463e61b1cfe5f462f1b609b28fbc1e4c4ff01e5"}, + {file = "gevent-24.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141a2b24ad14f7b9576965c0c84927fc85f824a9bb19f6ec1e61e845d87c9cd8"}, + {file = "gevent-24.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9202f22ef811053077d01f43cc02b4aaf4472792f9fd0f5081b0b05c926cca19"}, + {file = "gevent-24.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2955eea9c44c842c626feebf4459c42ce168685aa99594e049d03bedf53c2800"}, + {file = "gevent-24.2.1-cp38-cp38-win32.whl", hash = "sha256:44098038d5e2749b0784aabb27f1fcbb3f43edebedf64d0af0d26955611be8d6"}, + {file = "gevent-24.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:117e5837bc74a1673605fb53f8bfe22feb6e5afa411f524c835b2ddf768db0de"}, + {file = "gevent-24.2.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:2ae3a25ecce0a5b0cd0808ab716bfca180230112bb4bc89b46ae0061d62d4afe"}, + {file = "gevent-24.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ceb59986456ce851160867ce4929edaffbd2f069ae25717150199f8e1548b8"}, + {file = "gevent-24.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2e9ac06f225b696cdedbb22f9e805e2dd87bf82e8fa5e17756f94e88a9d37cf7"}, + {file = "gevent-24.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:90cbac1ec05b305a1b90ede61ef73126afdeb5a804ae04480d6da12c56378df1"}, + {file = "gevent-24.2.1-cp39-cp39-win32.whl", hash = "sha256:782a771424fe74bc7e75c228a1da671578c2ba4ddb2ca09b8f959abdf787331e"}, + {file = "gevent-24.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:3adfb96637f44010be8abd1b5e73b5070f851b817a0b182e601202f20fa06533"}, + {file = "gevent-24.2.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:7b00f8c9065de3ad226f7979154a7b27f3b9151c8055c162332369262fc025d8"}, + {file = "gevent-24.2.1.tar.gz", hash = "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056"}, +] + +[package.dependencies] +cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = [ + {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}, + {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""}, +] +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0)"] +recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] +test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests"] + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "gwcs" +version = "0.18.3" +description = "Generalized World Coordinate System" +optional = false +python-versions = ">=3.8" +files = [ + {file = "gwcs-0.18.3-py3-none-any.whl", hash = "sha256:e9b9f92049870573c3d1c2d7a9e84a8b4136da673c9004a1b827db5b93d54967"}, + {file = "gwcs-0.18.3.tar.gz", hash = "sha256:a466a057624c6a0507468b00c7991d3eef848c64f41d6ad11344c8fa6a99fe55"}, +] + +[package.dependencies] +asdf = ">=2.8.1" +asdf-astropy = ">=0.2.0" +asdf-wcs-schemas = "*" +astropy = ">=5.1" +numpy = "*" +scipy = "*" + +[package.extras] +docs = ["sphinx", "sphinx-asdf", "sphinx-astropy", "sphinx-automodapi", "sphinx-rtd-theme", "stsci-rtd-theme"] +test = ["ci-watson (>=0.3.0)", "codecov", "pytest (>=4.6.0)", "pytest-astropy"] + +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.1.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "5.3.0" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"}, + {file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.0.1" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.0.1-py3-none-any.whl", hash = "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664"}, + {file = "jaraco_functools-4.0.1.tar.gz", hash = "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.classes", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsonschema" +version = "4.17.3" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, +] + +[package.dependencies] +attrs = ">=17.4.0" +importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} +pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "keyring" +version = "25.1.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "keyring-25.1.0-py3-none-any.whl", hash = "sha256:26fc12e6a329d61d24aa47b22a7c5c3f35753df7d8f2860973cf94f4e1fb3427"}, + {file = "keyring-25.1.0.tar.gz", hash = "sha256:7230ea690525133f6ad536a9b5def74a4bd52642abe594761028fc044d7c7893"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +completion = ["shtab (>=1.1.0)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + +[[package]] +name = "ndcube" +version = "2.1.4" +description = "A package for multi-dimensional contiguous and non-contiguous coordinate aware arrays." +optional = false +python-versions = ">=3.8" +files = [ + {file = "ndcube-2.1.4-py3-none-any.whl", hash = "sha256:4b798837d1c7296fbc8739ec96c3902dde756430fb0887cf9f83e2af5a5d5e59"}, + {file = "ndcube-2.1.4.tar.gz", hash = "sha256:42cbec3d2091d76e5c80f7e9ff986ddfa168a93a7ece3d24e81a0ac84e993db4"}, +] + +[package.dependencies] +astropy = ">=4.2" +gwcs = ">=0.15" +numpy = ">1.17" + +[package.extras] +all = ["matplotlib (>=3.2)", "mpl-animators (>=1.0)", "reproject (>=0.7.1)"] +dev = ["dask", "importlib-resources (<6)", "matplotlib", "matplotlib (>=3.2)", "mpl-animators (>=1.0)", "pytest", "pytest-astropy", "pytest-doctestplus (>=0.9.0)", "pytest-mpl (>=0.12)", "reproject (>=0.7.1)", "scipy", "sphinx", "sphinx-automodapi", "sphinx-changelog (>=1.1.0)", "sphinx-gallery", "sphinxext-opengraph", "sunpy (>=4.0.0)", "sunpy-sphinx-theme"] +docs = ["importlib-resources (<6)", "matplotlib", "pytest-doctestplus (>=0.9.0)", "sphinx", "sphinx-automodapi", "sphinx-changelog (>=1.1.0)", "sphinx-gallery", "sphinxext-opengraph", "sunpy (>=4.0.0)", "sunpy-sphinx-theme"] +plotting = ["matplotlib (>=3.2)", "mpl-animators (>=1.0)"] +reproject = ["reproject (>=0.7.1)"] +tests = ["dask", "pytest", "pytest-astropy", "pytest-mpl (>=0.12)", "scipy", "sunpy (>=4.0.0)"] + +[[package]] +name = "numpy" +version = "1.24.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, + {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, + {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, + {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, + {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, + {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, + {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, + {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, + {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, + {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, + {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, + {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, + {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, + {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, + {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, + {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, + {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, + {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, + {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, + {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, + {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, + {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, + {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, + {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, + {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, + {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, + {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, + {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, + {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, + {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, + {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, + {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, + {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pkgutil-resolve-name" +version = "1.3.10" +description = "Resolve a name to an object." +optional = false +python-versions = ">=3.6" +files = [ + {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, + {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, +] + +[[package]] +name = "plotly" +version = "5.21.0" +description = "An open-source, interactive data visualization library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "plotly-5.21.0-py3-none-any.whl", hash = "sha256:a33f41fd5922e45b2b253f795b200d14452eb625790bb72d0a72cf1328a6abbf"}, + {file = "plotly-5.21.0.tar.gz", hash = "sha256:69243f8c165d4be26c0df1c6f0b7b258e2dfeefe032763404ad7e7fb7d7c2073"}, +] + +[package.dependencies] +packaging = "*" +tenacity = ">=6.2.0" + +[[package]] +name = "prometheus-client" +version = "0.20.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "pycodestyle" +version = "2.11.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, + {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pyerfa" +version = "2.0.0.3" +description = "Python bindings for ERFA" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyerfa-2.0.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:676515861ca3f0cb9d7e693389233e7126413a5ba93a0cc4d36b8ca933951e8d"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a438865894d226247dcfcb60d683ae075a52716504537052371b2b73458fe4fc"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73bf7d23f069d47632a2feeb1e73454b10392c4f3c16116017a6983f1f0e9b2b"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:780b0f90adf500b8ba24e9d509a690576a7e8287e354cfb90227c5963690d3fc"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5447bb45ddedde3052693c86b941a4908f5dbeb4a697bda45b5b89de92cfb74a"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c24e7960c6cdd3fa3f4dba5f3444a106ad48c94ff0b19eebaee06a142c18c52"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-win32.whl", hash = "sha256:170a83bd0243da518119b846f296cf33fa03f1f884a88578c1a38560182cf64e"}, + {file = "pyerfa-2.0.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:51aa6e0faa4aa9ad8f0eef1c47fec76c5bebc0da7023a436089bdd6e5cfd625f"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fa9fceeb78057bfff7ae3aa6cdad3f1b193722de22bdbb75319256f4a9e2f76"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a8a2029fc62ff2369d01219f66a5ce6aed35ef33eddb06118b6c27e8573a9ed8"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da888da2c8db5a78273fbf0af4e74f04e2d312d371c3c021cf6c3b14fa60fe3b"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7354753addba5261ec1cbf1ba45784ed3a5c42da565ecc6e0aa36b7a17fa4689"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b55f7278c1dd362648d7956e1a5365ade5fed2fe5541b721b3ceb5271128892"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:23e5efcf96ed7161d74f79ca261d255e1f36988843d22cd97d8f60fe9c868d44"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-win32.whl", hash = "sha256:f0e9d0b122c454bcad5dbd0c3283b200783031d3f99ca9c550f49a7a7d4c41ea"}, + {file = "pyerfa-2.0.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:09af83540e23a7d61a8368b0514b3daa4ed967e1e52d0add4f501f58c500dd7f"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a07444fd53a5dd18d7955f86f8d9b1be9a68ceb143e1145c0019a310c913c04"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf7364e475cff1f973e2fcf6962de9df9642c8802b010e29b2c592ae337e3c5"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8458421166f6ffe2e259aaf4aaa6e802d6539649a40e3194a81d30dccdc167a"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96ea688341176ae6220cc4743cda655549d71e3e3b60c5a99d02d5912d0ddf55"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d56f6b5a0a3ed7b80d630041829463a872946df277259b5453298842d42a54a4"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-win32.whl", hash = "sha256:3ecb598924ddb4ea2b06efc6f1e55ca70897ed178a690e2eaa1e290448466c7c"}, + {file = "pyerfa-2.0.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1033fdb890ec70d3a511e20a464afc8abbea2180108f27b14d8f1d1addc38cbe"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d8c0dbb17119e52def33f9d6dbf2deaf2113ed3e657b6ff692df9b6a3598397"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a1edd2cbe4ead3bf9a51e578d5d83bdd7ab3b3ccb69e09b89a4c42aa5b35ffb"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04c3b715c924b6f972dd440a94a701a16a07700bc8ba9e88b1df765bdc36ad0"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d01c341c45b860ee5c7585ef003118c8015e9d65c30668d2f5bf657e1dcdd68"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24d89ead30edc6038408336ad9b696683e74c4eef550708fca6afef3ecd5b010"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0b8c5e74d48a505a014e855cd4c7be11604901d94fd6f34b685f6720b7b20ed8"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-win32.whl", hash = "sha256:2ccba04de166d81bdd3adcf10428d908ce2f3a56ed1c2767d740fec12680edbd"}, + {file = "pyerfa-2.0.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3df87743e27588c5bd5e1f3a886629b3277fdd418059ca048420d33169376775"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88aa1acedf298d255cc4b0740ee11a3b303b71763dba2f039d48abf0a95cf9df"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06d4f08e96867b1fc3ae9a9e4b38693ed0806463288efc41473ad16e14774504"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1819e0d95ff8dead80614f8063919d82b2dbb55437b6c0109d3393c1ab55954"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61f1097ac2ee8c15a2a636cdfb99340d708574d66f4610456bd457d1e6b852f4"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36f42ee01a62c6cbba58103e6f8e600b21ad3a71262dccf03d476efb4a20ea71"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3ecd6167b48bb8f1922fae7b49554616f2e7382748a4320ad46ebd7e2cc62f3d"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-win32.whl", hash = "sha256:7f9eabfefa5317ce58fe22480102902f10f270fc64a5636c010f7c0b7e0fb032"}, + {file = "pyerfa-2.0.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:4ea7ca03ecc440224c2bed8fb136fadf6cf8aea8ba67d717f635116f30c8cc8c"}, + {file = "pyerfa-2.0.0.3.tar.gz", hash = "sha256:d77fbbfa58350c194ccb99e5d93aa05d3c2b14d5aad8b662d93c6ad9fff41f39"}, +] + +[package.dependencies] +numpy = ">=1.17" + +[package.extras] +docs = ["sphinx-astropy (>=1.3)"] +test = ["pytest", "pytest-doctestplus (>=0.7)"] + +[[package]] +name = "pyerfa" +version = "2.0.1.4" +description = "Python bindings for ERFA" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ff112353944bf705342741f2fe41674f97154a302b0295eaef7381af92ad2b3a"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:900b266a3862baa9560d6b1b184dcc14e0e76d550ff70d32336d3989b2ed18ca"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:610d2bc314e140d876b93b1287c7c81685434873c8700cc3e1596193f77d1071"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e4508dd7ffd7b27b7f67168643764454887e990ca9e4584824f0e3ab5884c0f"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:83a44ba84ebfc3244412ecbf1065c087c382da84f1c3eee1f2a0638d9046ac96"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-win32.whl", hash = "sha256:46d3bed0ac666f08d8364b34a00b8c6595358d6c4f4532da8d13fac0e5227baa"}, + {file = "pyerfa-2.0.1.4-cp39-abi3-win_amd64.whl", hash = "sha256:bc3cf45967ac1af77a777deb050fb08bbc75256dd97ca6005e4d385358b7af40"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88a8d0f3608a66871615bd168fcddf674dce9f7568c239a03cf8d9936161d032"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9045e9f786c76cb55da86ada3405c378c32b88f6e3c6296cb288496ab374b068"}, + {file = "pyerfa-2.0.1.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:39cf838c9a21e40d4e3183bead65b3ce6af763c4a727f87d84909c9be7d3a33c"}, + {file = "pyerfa-2.0.1.4.tar.gz", hash = "sha256:acb8a6713232ea35c04bc6e40ac4e461dfcc817d395ef2a3c8051c1a33249dd3"}, +] + +[package.dependencies] +numpy = ">=1.19" + +[package.extras] +docs = ["sphinx-astropy (>=1.3)"] +test = ["pytest", "pytest-doctestplus (>=0.7)"] + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyrsistent" +version = "0.20.0" +description = "Persistent/Functional/Immutable data structures" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b"}, + {file = "pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f"}, + {file = "pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7"}, + {file = "pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224"}, + {file = "pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656"}, + {file = "pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee"}, + {file = "pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d"}, + {file = "pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174"}, + {file = "pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d"}, + {file = "pyrsistent-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86"}, + {file = "pyrsistent-0.20.0-cp38-cp38-win32.whl", hash = "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423"}, + {file = "pyrsistent-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d"}, + {file = "pyrsistent-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca"}, + {file = "pyrsistent-0.20.0-cp39-cp39-win32.whl", hash = "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f"}, + {file = "pyrsistent-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf"}, + {file = "pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b"}, + {file = "pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyvo" +version = "1.5.1" +description = "Astropy affiliated package for accessing Virtual Observatory data and services" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyvo-1.5.1-py3-none-any.whl", hash = "sha256:53bdedd06bb37e7d9dca899fe1cc067dc423f3585c6e4799b371f04a5555720e"}, + {file = "pyvo-1.5.1.tar.gz", hash = "sha256:0720810fe7b766ba53d10e9d9e4bb23bc967c151a6f392e22a6cbfe0d453a632"}, +] + +[package.dependencies] +astropy = ">=4.1" +requests = "*" + +[package.extras] +all = ["pillow"] +docs = ["sphinx-astropy"] +test = ["pytest-astropy", "pytest-doctestplus (>=0.13)", "requests-mock"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "recommonmark" +version = "0.7.1" +description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." +optional = false +python-versions = "*" +files = [ + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, +] + +[package.dependencies] +commonmark = ">=0.8.1" +docutils = ">=0.11" +sphinx = ">=1.3.1" + +[[package]] +name = "redis" +version = "5.0.3" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"}, + {file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "responses" +version = "0.25.0" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "responses-0.25.0-py3-none-any.whl", hash = "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a"}, + {file = "responses-0.25.0.tar.gz", hash = "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] + +[[package]] +name = "scipy" +version = "1.10.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = "<3.12,>=3.8" +files = [ + {file = "scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019"}, + {file = "scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f"}, + {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2"}, + {file = "scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd"}, + {file = "scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35"}, + {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d"}, + {file = "scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35"}, + {file = "scipy-1.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1"}, + {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f"}, + {file = "scipy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9"}, + {file = "scipy-1.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353"}, + {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601"}, + {file = "scipy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea"}, + {file = "scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5"}, +] + +[package.dependencies] +numpy = ">=1.19.5,<1.27.0" + +[package.extras] +dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"] +doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] +test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "semantic-version" +version = "2.10.0" +description = "A library implementing the 'SemVer' scheme." +optional = false +python-versions = ">=2.7" +files = [ + {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, + {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, +] + +[package.extras] +dev = ["Django (>=1.11)", "check-manifest", "colorama (<=0.4.1)", "coverage", "flake8", "nose2", "readme-renderer (<25.0)", "tox", "wheel", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "specutils" +version = "1.14.0" +description = "Package for spectroscopic astronomical data" +optional = false +python-versions = ">=3.8" +files = [ + {file = "specutils-1.14.0-py3-none-any.whl", hash = "sha256:71bfa3e3536d7630cc655f981728ec72ad0186400bb24b89991cdd60bd02488a"}, + {file = "specutils-1.14.0.tar.gz", hash = "sha256:0e0bbdf6687b5d9d93bbc222c2c73bc2d0692d22895b5604d26e4112219a801a"}, +] + +[package.dependencies] +asdf = ">=2.14.4" +asdf-astropy = ">=0.3" +astropy = ">=5.1" +gwcs = ">=0.18" +ndcube = ">=2.0" +numpy = ">=1.19" +scipy = ">=1.3" + +[package.extras] +docs = ["graphviz", "matplotlib", "sphinx-astropy"] +jwst = ["stdatamodels (>=1.1.0)"] +test = ["matplotlib", "pytest-astropy", "spectral-cube", "tox"] + +[[package]] +name = "sphinx" +version = "4.5.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, + {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.18" +imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +description = "Add a copy button to each of your code cells." +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, +] + +[package.dependencies] +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] + +[[package]] +name = "sphinx-panels" +version = "0.6.0" +description = "A sphinx extension for creating panels in a grid layout." +optional = false +python-versions = "*" +files = [ + {file = "sphinx-panels-0.6.0.tar.gz", hash = "sha256:d36dcd26358117e11888f7143db4ac2301ebe90873ac00627bf1fe526bf0f058"}, + {file = "sphinx_panels-0.6.0-py3-none-any.whl", hash = "sha256:bd64afaf85c07f8096d21c8247fc6fd757e339d1be97832c8832d6ae5ed2e61d"}, +] + +[package.dependencies] +docutils = "*" +sphinx = ">=2,<5" + +[package.extras] +code-style = ["pre-commit (>=2.7.0,<2.8.0)"] +live-dev = ["sphinx-autobuild", "web-compile (>=0.2.0,<0.3.0)"] +testing = ["pytest (>=6.0.1,<6.1.0)", "pytest-regressions (>=2.0.1,<2.1.0)"] +themes = ["myst-parser (>=0.12.9,<0.13.0)", "pydata-sphinx-theme (>=0.4.0,<0.5.0)", "sphinx-book-theme (>=0.0.36,<0.1.0)", "sphinx-rtd-theme"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.3.0" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, + {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, +] + +[package.dependencies] +docutils = "<0.19" +sphinx = ">=1.6,<8" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sqlparse" +version = "0.5.0" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, + {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "watchdog" +version = "4.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, + {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, + {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, + {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, + {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, + {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, + {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, + {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "watchdog-gevent" +version = "0.1.1" +description = "A gevent-based observer for watchdog." +optional = false +python-versions = "*" +files = [ + {file = "watchdog_gevent-0.1.1-py3-none-any.whl", hash = "sha256:dcebd07668b472790ad0e7a7b40ad8c365e156c40cf54ffd5499c65c65c4f66f"}, + {file = "watchdog_gevent-0.1.1.tar.gz", hash = "sha256:71dced5ec451b3deafd1dc73baaf52a7ce4d54d9a1eb606f9370235125f2424a"}, +] + +[package.dependencies] +gevent = ">=1.1" +watchdog = ">=0.8" + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[[package]] +name = "zipp" +version = "3.18.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "zope-event" +version = "5.0" +description = "Very basic event publishing system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, + {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner"] + +[[package]] +name = "zope-interface" +version = "6.3" +description = "Interfaces for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zope.interface-6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021"}, + {file = "zope.interface-6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785"}, + {file = "zope.interface-6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a"}, + {file = "zope.interface-6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21"}, + {file = "zope.interface-6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1"}, + {file = "zope.interface-6.3-cp310-cp310-win_amd64.whl", hash = "sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb"}, + {file = "zope.interface-6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39"}, + {file = "zope.interface-6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299"}, + {file = "zope.interface-6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130"}, + {file = "zope.interface-6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10"}, + {file = "zope.interface-6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e"}, + {file = "zope.interface-6.3-cp311-cp311-win_amd64.whl", hash = "sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061"}, + {file = "zope.interface-6.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5"}, + {file = "zope.interface-6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b"}, + {file = "zope.interface-6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e"}, + {file = "zope.interface-6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920"}, + {file = "zope.interface-6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c"}, + {file = "zope.interface-6.3-cp312-cp312-win_amd64.whl", hash = "sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5"}, + {file = "zope.interface-6.3-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85"}, + {file = "zope.interface-6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e"}, + {file = "zope.interface-6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e"}, + {file = "zope.interface-6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d"}, + {file = "zope.interface-6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12"}, + {file = "zope.interface-6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86"}, + {file = "zope.interface-6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0"}, + {file = "zope.interface-6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91"}, + {file = "zope.interface-6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5"}, + {file = "zope.interface-6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1"}, + {file = "zope.interface-6.3-cp38-cp38-win_amd64.whl", hash = "sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af"}, + {file = "zope.interface-6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92"}, + {file = "zope.interface-6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf"}, + {file = "zope.interface-6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83"}, + {file = "zope.interface-6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c"}, + {file = "zope.interface-6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9"}, + {file = "zope.interface-6.3-cp39-cp39-win_amd64.whl", hash = "sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6"}, + {file = "zope.interface-6.3.tar.gz", hash = "sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] +test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] +testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8.1,<3.12" +content-hash = "fd1ac8d4bb3dd9f08decdbafa1af45b0d685abde37b2499bfd35840a5a883e29" From 1449b557b8fcb8a4f5b75aa5b9716975867012e3 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Mon, 22 Apr 2024 17:53:44 -0700 Subject: [PATCH 50/69] add unique and name_check methods to match manager --- tom_targets/base_models.py | 12 ++++++++++-- tom_targets/models.py | 5 +++-- tom_targets/tests/tests.py | 18 +++++++++++------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 3ac8b8161..40756f4d4 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -47,6 +47,14 @@ class TargetMatchManager(models.Manager): and parentheses. Additional matching functions can be added. """ + def check_unique(self, name=''): + queryset = self.check_for_fuzzy_match(name) + return queryset + + def check_for_name_match(self, name): + queryset = self.check_for_fuzzy_match(name) + return queryset + def check_for_fuzzy_match(self, name): """ Check for case-insensitive names ignoring spaces, dashes, underscore, and parentheses. @@ -327,11 +335,11 @@ def validate_unique(self, *args, **kwargs): super().validate_unique(*args, **kwargs) # Check DB for similar target/alias names. - matches = self.__class__.matches.check_for_fuzzy_match(self.name) + matches = self.__class__.matches.check_unique(self.name) for match in matches: # Ignore the fact that this target's name matches itself. if match.id != self.id: - raise ValidationError(f'Target with Name or alias similar to {self.name} already exists') + raise ValidationError(f'A Target matching {self.name} already exists. ({match.name})') # Alias Check only necessary when updating target existing target. Reverse relationships require Primary Key. # If nothing has changed for the Target, do not validate against existing aliases. if self.pk and self.name != self.__class__.objects.get(pk=self.pk).name: diff --git a/tom_targets/models.py b/tom_targets/models.py index 0c637a1ad..5dce56d9a 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -75,9 +75,10 @@ def validate_unique(self, *args, **kwargs): f'(target_id={self.target.id})') # Check DB for similar target/alias names. - matches = Target.matches.check_for_fuzzy_match(self.name) + matches = Target.matches.check_for_name_match(self.name) if matches: - raise ValidationError(f'Target with Name or alias similar to {self.name} already exists') + raise ValidationError(f'Target with Name or alias similar to {self.name} already exists.' + f' ({matches.first().name})') class TargetExtra(models.Model): diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index d3345d20c..5323a73cc 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -513,8 +513,9 @@ def test_create_targets_with_conflicting_aliases(self): self.assertContains(second_response, 'Target name with this Alias already exists.') def test_create_target_name_conflicting_with_existing_aliases(self): + original_name = 'multiple_names_target' target_data = { - 'name': 'multiple_names_target', + 'name': original_name, 'type': Target.SIDEREAL, 'ra': 113.456, 'dec': -22.1, @@ -540,8 +541,8 @@ def test_create_target_name_conflicting_with_existing_aliases(self): for i, name in enumerate(names): target_data.pop(f'aliases-{i}-name') second_response = self.client.post(reverse('targets:create'), data=target_data, follow=True) - self.assertContains(second_response, f'Target with Name or alias similar to {target_data["name"]} ' - f'already exists') + self.assertContains(second_response, f'A Target matching {target_data["name"]} already exists. ' + f'({original_name})') self.assertFalse(Target.objects.filter(name=target_data['name']).exists()) def test_create_target_alias_conflicting_with_existing_target_name(self): @@ -602,8 +603,9 @@ def test_create_target_alias_conflicting_with_target_name(self): self.assertFalse(TargetName.objects.filter(name=target_data['name']).exists()) def test_create_targets_with_fuzzy_conflicting_names(self): + original_name = 'fuzzy_name_Target' target_data = { - 'name': 'fuzzy_name_Target', + 'name': original_name, 'type': Target.SIDEREAL, 'ra': 113.456, 'dec': -22.1, @@ -624,11 +626,12 @@ def test_create_targets_with_fuzzy_conflicting_names(self): for name in names: target_data['name'] = name second_response = self.client.post(reverse('targets:create'), data=target_data, follow=True) - self.assertContains(second_response, f'Target with Name or alias similar to {name} already exists') + self.assertContains(second_response, f'A Target matching {name} already exists. ({original_name})') def test_create_alias_fuzzy_conflict_with_existing_target_name(self): + original_name = 'John' target_data = { - 'name': 'John', + 'name': original_name, 'type': Target.SIDEREAL, 'ra': 113.456, 'dec': -22.1, @@ -651,7 +654,8 @@ def test_create_alias_fuzzy_conflict_with_existing_target_name(self): target_data['name'] = 'multiple_names_target' target_data['aliases-TOTAL_FORMS'] = 2 second_response = self.client.post(reverse('targets:create'), data=target_data, follow=True) - self.assertContains(second_response, f'Target with Name or alias similar to {names[0]} already exists') + self.assertContains(second_response, f'Target with Name or alias similar to {names[0]} already exists. ' + f'({original_name})') class TestTargetUpdate(TestCase): From 1f34208345321698ebf3fd29179035091c91f7c2 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 11:02:22 -0700 Subject: [PATCH 51/69] add exact match finder --- tom_dataproducts/alertstreams/hermes.py | 2 +- tom_targets/base_models.py | 23 ++++++++++++++++------- tom_targets/models.py | 2 +- tom_targets/tests/test_utils.py | 8 ++++---- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/tom_dataproducts/alertstreams/hermes.py b/tom_dataproducts/alertstreams/hermes.py index bb86c390f..e9e7dd6b0 100644 --- a/tom_dataproducts/alertstreams/hermes.py +++ b/tom_dataproducts/alertstreams/hermes.py @@ -208,7 +208,7 @@ def hermes_alert_handler(alert, metadata): for row in photometry_table: if row['target_name'] != target_name: target_name = row['target_name'] - query = Target.matches.check_for_fuzzy_match(target_name) + query = Target.matches.get_name_match(target_name) if query: target = query[0] else: diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 40756f4d4..f7167abf0 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -47,15 +47,24 @@ class TargetMatchManager(models.Manager): and parentheses. Additional matching functions can be added. """ - def check_unique(self, name=''): - queryset = self.check_for_fuzzy_match(name) + def check_unique(self, target): + queryset = self.get_name_match(target.name) return queryset - def check_for_name_match(self, name): - queryset = self.check_for_fuzzy_match(name) + def get_name_match(self, name): + queryset = self.check_for_fuzzy_name_match(name) return queryset - def check_for_fuzzy_match(self, name): + def check_for_exact_name_match(self, name): + """ + Returns a queryset exactly matching name that is received + :param name: The string against which target names will be matched. + :return: queryset containing matching Target(s). + """ + queryset = super().get_queryset().filter(name=name) + return queryset + + def check_for_fuzzy_name_match(self, name): """ Check for case-insensitive names ignoring spaces, dashes, underscore, and parentheses. :param name: The string against which target names and aliases will be matched. @@ -71,7 +80,7 @@ def check_for_fuzzy_match(self, name): return queryset def make_simple_name(self, name): - """Create a simplified name to be used for comparison in check_for_fuzzy_match.""" + """Create a simplified name to be used for comparison in check_for_fuzzy_name_match.""" return name.lower().replace(" ", "").replace("-", "").replace("_", "").replace("(", "").replace(")", "") @@ -335,7 +344,7 @@ def validate_unique(self, *args, **kwargs): super().validate_unique(*args, **kwargs) # Check DB for similar target/alias names. - matches = self.__class__.matches.check_unique(self.name) + matches = self.__class__.matches.check_unique(self) for match in matches: # Ignore the fact that this target's name matches itself. if match.id != self.id: diff --git a/tom_targets/models.py b/tom_targets/models.py index 5dce56d9a..1c748453f 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -75,7 +75,7 @@ def validate_unique(self, *args, **kwargs): f'(target_id={self.target.id})') # Check DB for similar target/alias names. - matches = Target.matches.check_for_name_match(self.name) + matches = Target.matches.get_name_match(self.name) if matches: raise ValidationError(f'Target with Name or alias similar to {self.name} already exists.' f' ({matches.first().name})') diff --git a/tom_targets/tests/test_utils.py b/tom_targets/tests/test_utils.py index cc9737d1f..d303fb905 100644 --- a/tom_targets/tests/test_utils.py +++ b/tom_targets/tests/test_utils.py @@ -1,16 +1,16 @@ -from django.db import models +from tom_targets.base_models import TargetMatchManager -class StrictMatch(models.Manager): +class StrictMatch(TargetMatchManager): """ Return Queryset for target with name matching string. """ - def check_for_fuzzy_match(self, name): + def get_name_match(self, name): """ Returns a queryset exactly matching name that is received :param name: The string against which target names will be matched. :return: queryset containing matching Target(s). """ - queryset = super().get_queryset().filter(name=name) + queryset = self.check_for_exact_name_match(name) return queryset From 60808722bf4b9bf443046d4f215d6636609bea87 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 13:53:28 -0700 Subject: [PATCH 52/69] add tests --- tom_targets/base_models.py | 23 ++++++++++++++- tom_targets/tests/test_utils.py | 18 ++++++++++++ tom_targets/tests/tests.py | 52 +++++++++++++++++++++------------ 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index f7167abf0..51e224efc 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -3,10 +3,12 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.db import models, transaction +from django.db.models.functions.math import ACos, Cos, Radians, Pi, Sin from django.forms.models import model_to_dict from django.urls import reverse from django.utils.module_loading import import_string from guardian.shortcuts import assign_perm +from math import radians from tom_common.hooks import run_hook @@ -47,7 +49,7 @@ class TargetMatchManager(models.Manager): and parentheses. Additional matching functions can be added. """ - def check_unique(self, target): + def check_unique(self, target, *args, **kwargs): queryset = self.get_name_match(target.name) return queryset @@ -55,6 +57,25 @@ def get_name_match(self, name): queryset = self.check_for_fuzzy_name_match(name) return queryset + def check_for_nearby_match(self, ra, dec, radius): + radius /= 3600 # Convert radius from arcseconds to degrees + double_radius = radius * 2 + # print(super().get_queryset()) + # queryset = super().get_queryset().all().filter(ra__gte=ra - double_radius, ra__lte=ra + double_radius) + queryset = super().get_queryset().filter( + ra__gte=ra - double_radius, ra__lte=ra + double_radius, + dec__gte=dec - double_radius, dec__lte=dec + double_radius + ) + + separation = models.ExpressionWrapper( + 180 * ACos( + (Sin(radians(dec)) * Sin(Radians('dec'))) + + (Cos(radians(dec)) * Cos(Radians('dec')) * Cos(radians(ra) - Radians('ra'))) + ) / Pi(), models.FloatField() + ) + + return queryset.annotate(separation=separation).filter(separation__lte=radius) + def check_for_exact_name_match(self, name): """ Returns a queryset exactly matching name that is received diff --git a/tom_targets/tests/test_utils.py b/tom_targets/tests/test_utils.py index d303fb905..e2c96ac16 100644 --- a/tom_targets/tests/test_utils.py +++ b/tom_targets/tests/test_utils.py @@ -14,3 +14,21 @@ def get_name_match(self, name): """ queryset = self.check_for_exact_name_match(name) return queryset + + +class ConeSearchManager(TargetMatchManager): + """ + Return Queryset for target with name matching string. + """ + + def check_unique(self, target, *args, **kwargs): + """ + Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of + the target that is received + :param target: The target object to be checked. + :return: queryset containing matching Target(s). + """ + queryset = self.super().check_unique(target, *args, **kwargs) + radius = 2 + cone_search_queryset = self.check_for_nearby_match(target.ra, target.dec, radius) + return queryset | cone_search_queryset diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index 5323a73cc..84db3d20c 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -874,25 +874,39 @@ def test_raw_update_process(self): with self.assertRaises(ValidationError): new_alias.full_clean() - @override_settings(MATCH_MANAGERS={'Target': 'tom_targets.tests.test_utils.StrictMatch'}) - def test_update_with_strict_matching(self): - self.form_data.update({ - 'targetextra_set-TOTAL_FORMS': 1, - 'targetextra_set-INITIAL_FORMS': 0, - 'targetextra_set-MIN_NUM_FORMS': 0, - 'targetextra_set-MAX_NUM_FORMS': 1000, - 'targetextra_set-0-key': 'redshift', - 'targetextra_set-0-value': '3', - 'aliases-TOTAL_FORMS': 1, - 'aliases-INITIAL_FORMS': 0, - 'aliases-MIN_NUM_FORMS': 0, - 'aliases-MAX_NUM_FORMS': 1000, - 'aliases-0-name': 'testtargetname2' - }) - self.client.post(reverse('targets:update', kwargs={'pk': self.target.id}), data=self.form_data) - self.target.refresh_from_db() - self.assertTrue(self.target.targetextra_set.filter(key='redshift').exists()) - self.assertTrue(self.target.aliases.filter(name='testtargetname2').exists()) + +class TestTargetMatchManager(TestCase): + def setUp(self): + self.form_data = { + 'name': 'testtarget', + 'type': Target.SIDEREAL, + 'ra': 113.456, + 'dec': -22.1 + } + user = User.objects.create(username='testuser') + self.target = Target.objects.create(**self.form_data) + assign_perm('tom_targets.change_target', user, self.target) + self.client.force_login(user) + + def test_strict_matching(self): + fuzzy_name = "test_target" + fuzzy_matches = Target.matches.check_for_fuzzy_name_match(fuzzy_name) + strict_matches = Target.matches.check_for_exact_name_match(fuzzy_name) + self.assertTrue(fuzzy_matches.exists()) + self.assertFalse(strict_matches.exists()) + + def test_cone_search_matching(self): + ra = 113.456 + dec = -22.1 + radius = 1 + matches = Target.matches.check_for_nearby_match(ra, dec, radius) + self.assertTrue(matches.exists()) + ra += 0.01 + matches = Target.matches.check_for_nearby_match(ra, dec, radius) + self.assertFalse(matches.exists()) + radius += 100 + matches = Target.matches.check_for_nearby_match(ra, dec, radius) + self.assertTrue(matches.exists()) class TestTargetImport(TestCase): From ad7124474568e7fcbb7330789ed060d8f28d9073 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 14:42:46 -0700 Subject: [PATCH 53/69] add Base Target model into docs. --- docs/api/tom_targets/models.rst | 4 ++++ docs/targets/index.rst | 4 +++- docs/targets/target_fields.rst | 2 +- docs/targets/target_matcher.rst | 2 +- tom_setup/templates/tom_setup/settings.tmpl | 8 ++++++++ tom_targets/tests/test_utils.py | 2 +- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/api/tom_targets/models.rst b/docs/api/tom_targets/models.rst index da54c2a06..170e4b3f4 100644 --- a/docs/api/tom_targets/models.rst +++ b/docs/api/tom_targets/models.rst @@ -3,6 +3,10 @@ Models .. autosummary:: +.. automodule:: tom_targets.base_models + :members: + :special-members: + .. automodule:: tom_targets.models :members: :special-members: \ No newline at end of file diff --git a/docs/targets/index.rst b/docs/targets/index.rst index 3ba453cf1..72fccbbde 100644 --- a/docs/targets/index.rst +++ b/docs/targets/index.rst @@ -6,8 +6,10 @@ Targets :hidden: target_fields + target_matcher ../api/tom_targets/models ../api/tom_targets/views + ../api/tom_targets/groups The ``Target``, along with the associated ``TargetList``, ``TargetExtra``, and ``TargetName``, are the core models of the @@ -19,7 +21,7 @@ defaults do not suffice. :doc:`Adding Custom Target Matcher ` - Learn how to replace or modify the TargetMatchManager if more options are needed. -:doc:`Target API <../api/tom_targets/models>` - Take a look at the available properties for a ``Target`` and its associated models. +:doc:`Target Models <../api/tom_targets/models>` - Take a look at the available properties for a ``Target`` and its associated models. :doc:`Target Views <../api/tom_targets/views>` - Familiarize yourself with the available Target Views. diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index b0fca205a..ea9b9704a 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -209,7 +209,7 @@ as the built-in fields from any custom code you write, the API, or from the admi Transferring existing ``Extra Field`` Data to your ``Target`` Fields -=================================================================== +==================================================================== If you have been using ``Extra Fields`` and have now created a custom target model, you may want to transfer the data from the ``Extra Fields`` to the new fields in your custom target model. This can be done by running a management diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index f37931ae1..7f78564c9 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -2,7 +2,7 @@ Adding Custom Target Matcher ---------------------------- The role of the ``TargetMatchManager`` is to return a queryset of targets that match a given set of parameters. -By default, the TOM Toolkit includes a ``TargetMatchManager`` that contains a ``check_for_fuzzy_match`` function that +By default, the TOM Toolkit includes a ``TargetMatchManager`` that contains ``check_for_fuzzy_match`` function that will return a queryset of ``TargetNames`` that are "similar" to a given string. This function will check for case-insensitive aliases while ignoring spaces, dashes, underscore, and parentheses. This function is used during ``validate_unique`` when the target is saved to ensure that redundant targets are not added. diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index 864e28ede..9cfd80bd4 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -197,6 +197,14 @@ TARGET_TYPE = '{{ TARGET_TYPE }}' # Set to the full path of a custom target model to extend the BaseTarget Model with custom fields. # TARGET_MODEL_CLASS = '{{ CUSTOM_CODE_APP_NAME }}.models.UserDefinedTarget' +# Define MATCH_MANAGERS here. This is a dictionary that contains a dotted module path to the desired match manager +# for a given model. +# For example: +# MATCH_MANAGERS = { +# "Target": "custom_code.match_managers.MyCustomTargetMatchManager" +# } +MATCH_MANAGERS = {} + FACILITIES = { 'LCO': { 'portal_url': 'https://observe.lco.global', diff --git a/tom_targets/tests/test_utils.py b/tom_targets/tests/test_utils.py index e2c96ac16..8c28662db 100644 --- a/tom_targets/tests/test_utils.py +++ b/tom_targets/tests/test_utils.py @@ -28,7 +28,7 @@ def check_unique(self, target, *args, **kwargs): :param target: The target object to be checked. :return: queryset containing matching Target(s). """ - queryset = self.super().check_unique(target, *args, **kwargs) + queryset = super().check_unique(target, *args, **kwargs) radius = 2 cone_search_queryset = self.check_for_nearby_match(target.ra, target.dec, radius) return queryset | cone_search_queryset From c7332acbe398c2911a3871a7e23db54e0465f0bf Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 15:24:15 -0700 Subject: [PATCH 54/69] update docs --- docs/targets/index.rst | 2 +- docs/targets/target_matcher.rst | 84 +++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/docs/targets/index.rst b/docs/targets/index.rst index 72fccbbde..57acbbe70 100644 --- a/docs/targets/index.rst +++ b/docs/targets/index.rst @@ -18,7 +18,7 @@ TOM Toolkit. The ``Target`` defines the concept of an astronomical target. :doc:`Adding Custom Target Fields ` - Learn how to add custom fields to your TOM Targets if the defaults do not suffice. -:doc:`Adding Custom Target Matcher ` - Learn how to replace or modify the TargetMatchManager if more +:doc:`Customizing a Target Matcher ` - Learn how to replace or modify the TargetMatchManager if more options are needed. :doc:`Target Models <../api/tom_targets/models>` - Take a look at the available properties for a ``Target`` and its associated models. diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index 7f78564c9..e936d70f3 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -2,12 +2,9 @@ Adding Custom Target Matcher ---------------------------- The role of the ``TargetMatchManager`` is to return a queryset of targets that match a given set of parameters. -By default, the TOM Toolkit includes a ``TargetMatchManager`` that contains ``check_for_fuzzy_match`` function that -will return a queryset of ``TargetNames`` that are "similar" to a given string. This function will check for -case-insensitive aliases while ignoring spaces, dashes, underscore, and parentheses. This function is used during -``validate_unique`` when the target is saved to ensure that redundant targets are not added. - -Under certain circumstances a user may wish to modify or add to this behavior. +By default, the TOM Toolkit includes a ``TargetMatchManager`` that contains several methods that are detailed +in :doc:`Target: Models <../api/tom_targets/models>`. These functions can be modified or replaced by a user to +alter the conditions under which a target is considered a match. Using the TargetMatchManager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -15,8 +12,22 @@ Using the TargetMatchManager The ``TargetMatchManager`` is a django model manager defined as ``Target.matches``. Django model managers are described in more detail in the `Django Docs `_. -Overriding the TargetMatchManager -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You can use the ``TargetMatchManager`` to return a queryset of targets that satisfy a cone search with the following: + +.. code:: python + + from tom_targets.models import Target + + # Define the center of the cone search + ra = 10.68458 # Degrees + dec = 41.26906 # Degrees + radius = 12 # Arcseconds + + # Get the queryset of targets that match the cone search + targets = Target.matches.check_for_nearby_match(ra, dec, radius) + +Extending the TargetMatchManager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To start, find the ``MATCH_MANAGERS`` definition in your ``settings.py``: @@ -26,32 +37,55 @@ To start, find the ``MATCH_MANAGERS`` definition in your ``settings.py``: # for a given model. # For example: # MATCH_MANAGERS = { - # "Target": "my_custom_code.match_managers.MyCustomTargetMatchManager" + # "Target": "custom_code.match_managers.CustomTargetMatchManager" # } MATCH_MANAGERS = {} Add the path to your custom ``TargetMatchManager`` to the "Target" key of the MATCH_MANAGERS dictionary as shown in the example. -Your can the override the default ``TargetMatchManager`` by writing your own in the location you used above. +The following code provides an example of a custom ``TargetMatchManager`` that checks for exact name matches and +requires that a target must have an RA and DEC more than 2" away from any other target in the database to be considered +unique: -**Remember** the ``TargetMatchManager`` must contain a ``check_for_fuzzy_match`` function and return a queryset. -See the following example for only checking for exact name matches: +.. code-block:: python + :caption: match_managers.py + :linenos: + + from tom_targets.base_models import TargetMatchManager -.. code:: python - class CustomTargetMatchManager(models.Manager): - """ - Return Queryset for target with name matching string. - """ - def check_for_fuzzy_match(self, name): + class CustomTargetMatchManager(TargetMatchManager): """ - Returns a queryset exactly matching name that is received - :param name: The string against which target names will be matched. - :return: queryset containing matching Target(s). + Custom Match Manager for extending the built in TargetMatchManager. """ - queryset = super().get_queryset().filter(name=name) - return queryset -This might be useful if a user is experiencing performance issues when ingesting targets or does not wish to allow for -a target matching to similar strings. + def check_unique(self, target, *args, **kwargs): + """ + Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of + the target that is received + :param target: The target object to be checked. + :return: queryset containing matching Target(s). + """ + queryset = super().check_unique(target, *args, **kwargs) + radius = 2 + cone_search_queryset = self.check_for_nearby_match(target.ra, target.dec, radius) + return queryset | cone_search_queryset + + def get_name_match(self, name): + """ + Returns a queryset exactly matching name that is received + :param name: The string against which target names will be matched. + :return: queryset containing matching Target(s). + """ + queryset = self.check_for_exact_name_match(name) + return queryset + +Your ``MatchManager`` should extend the ``base_model.TargetMatchManager`` which will contain both a ``check_unique`` +method and a ``get_name_match`` method, both of which should return a queryset. These methods can be modified or +extended, as in the above example, as needed. + +.. note:: + The default behavior for ``get_name_match`` is to perform a "fuzzy match". This can be computationally expensive + for large databases. If you have experienced this issue, you can override the ``get_name_match`` method to only + return exact matches using the above example. From 135f4ae9940d18131bfda6ad323db9815e48ae2a Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 15:45:09 -0700 Subject: [PATCH 55/69] update doc strings --- tom_targets/base_models.py | 57 ++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 51e224efc..f186bc350 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -43,21 +43,57 @@ class TargetMatchManager(models.Manager): """ - Search for matches amongst Target names and Aliases + Search for matches amongst Target objects. Return Queryset containing relevant TARGET matches. - NOTE: check_for_fuzzy_match looks for Target names and aliases ignoring capitalization, spaces, dashes, underscores, - and parentheses. Additional matching functions can be added. + + NOTE: + ``check_unique`` and ``get_name_match`` are used throughout the code to determine if a target or a name is + unique. These functions can be overridden in a subclass to provide custom matching logic. Examples of this can be + found in the documentation (https://tom-toolkit.readthedocs.io/en/stable/targets/target_matcher.html). """ def check_unique(self, target, *args, **kwargs): + """ + Check if any other targets match the given target. By default, this checks for a match in the name field using + the get_name_match function. + This function is used in the ``Target.validate_unique()`` function to check for uniqueness. + + :param target: The target object to be checked against. + + :return: queryset containing matching Target(s). + + """ queryset = self.get_name_match(target.name) return queryset def get_name_match(self, name): + """ + Returns a queryset of matching names. By default, this checks for a fuzzy match using the + ``check_for_fuzzy_name_match`` function. + + :param name: The string against which target names will be matched. + + :return: queryset containing matching Target(s). + """ queryset = self.check_for_fuzzy_name_match(name) return queryset - def check_for_nearby_match(self, ra, dec, radius): + def check_for_nearby_match(self, ra: float, dec: float, radius: float): + """ + Returns a queryset containing any targets that are within the given radius of the given ra and dec. + + :param ra: The right ascension of the target in degrees. + :type ra: float + + :param dec: The declination of the target in degrees. + :type dec: float + + :param radius: The radius in arcseconds within which to search for targets. + :type radius: float + + :return: queryset containing matching Target(s). + + """ radius /= 3600 # Convert radius from arcseconds to degrees double_radius = radius * 2 # print(super().get_queryset()) @@ -79,7 +115,9 @@ def check_for_nearby_match(self, ra, dec, radius): def check_for_exact_name_match(self, name): """ Returns a queryset exactly matching name that is received + :param name: The string against which target names will be matched. + :return: queryset containing matching Target(s). """ queryset = super().get_queryset().filter(name=name) @@ -88,7 +126,9 @@ def check_for_exact_name_match(self, name): def check_for_fuzzy_name_match(self, name): """ Check for case-insensitive names ignoring spaces, dashes, underscore, and parentheses. + :param name: The string against which target names and aliases will be matched. + :return: queryset containing matching Targets. Will return targets even when matched value is an alias. """ simple_name = self.make_simple_name(name) @@ -101,7 +141,14 @@ def check_for_fuzzy_name_match(self, name): return queryset def make_simple_name(self, name): - """Create a simplified name to be used for comparison in check_for_fuzzy_name_match.""" + """ + Create a simplified name to be used for comparison in check_for_fuzzy_name_match. + By default, this method removes capitalization, spaces, dashes, underscores, and parentheses from the name. + + :param name: The string to be simplified. + + :return: A simplified string version of the given name. + """ return name.lower().replace(" ", "").replace("-", "").replace("_", "").replace("(", "").replace(")", "") From 39e919a6486b91df5d684c5655a6ce55e624a14c Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 15:58:38 -0700 Subject: [PATCH 56/69] fix linting --- tom_targets/base_models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index f186bc350..8b8ddd917 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -46,10 +46,10 @@ class TargetMatchManager(models.Manager): Search for matches amongst Target objects. Return Queryset containing relevant TARGET matches. - NOTE: + NOTE: ``check_unique`` and ``get_name_match`` are used throughout the code to determine if a target or a name is - unique. These functions can be overridden in a subclass to provide custom matching logic. Examples of this can be - found in the documentation (https://tom-toolkit.readthedocs.io/en/stable/targets/target_matcher.html). + unique. These functions can be overridden in a subclass to provide custom matching logic. Examples of this can + be found in the documentation (https://tom-toolkit.readthedocs.io/en/stable/targets/target_matcher.html). """ def check_unique(self, target, *args, **kwargs): From 1281de16bc2f841036f756f720e448e08173ad2a Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 23 Apr 2024 16:04:04 -0700 Subject: [PATCH 57/69] update doc heading --- docs/targets/target_matcher.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index e936d70f3..4854c9711 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -1,4 +1,4 @@ -Adding Custom Target Matcher +Customizing a Target Matcher ---------------------------- The role of the ``TargetMatchManager`` is to return a queryset of targets that match a given set of parameters. From 6f92975618a9def7351923e973c04214152d4d90 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 25 Apr 2024 08:39:05 -0700 Subject: [PATCH 58/69] make sure not to do cone search for Non-sidereal targets. --- tom_targets/base_models.py | 6 ++++-- tom_targets/tests/tests.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 8b8ddd917..ce0efbca3 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -94,10 +94,12 @@ def check_for_nearby_match(self, ra: float, dec: float, radius: float): :return: queryset containing matching Target(s). """ + # Return an empty queryset if any of the parameters are None such as for a NonSidereal target + if ra is None or dec is None or radius is None: + return self.get_queryset().none() + radius /= 3600 # Convert radius from arcseconds to degrees double_radius = radius * 2 - # print(super().get_queryset()) - # queryset = super().get_queryset().all().filter(ra__gte=ra - double_radius, ra__lte=ra + double_radius) queryset = super().get_queryset().filter( ra__gte=ra - double_radius, ra__lte=ra + double_radius, dec__gte=dec - double_radius, dec__lte=dec + double_radius diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index 84db3d20c..af4b21468 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -899,14 +899,20 @@ def test_cone_search_matching(self): ra = 113.456 dec = -22.1 radius = 1 + # Test for exact match matches = Target.matches.check_for_nearby_match(ra, dec, radius) self.assertTrue(matches.exists()) + # Test for slightly off match ra += 0.01 matches = Target.matches.check_for_nearby_match(ra, dec, radius) self.assertFalse(matches.exists()) + # Test for match with larger radius radius += 100 matches = Target.matches.check_for_nearby_match(ra, dec, radius) self.assertTrue(matches.exists()) + # Test for moving objects with no RA/DEC + matches = Target.matches.check_for_nearby_match(None, None, radius) + self.assertFalse(matches.exists()) class TestTargetImport(TestCase): From f68ffc197b30a689a80e132cc2784644d0a86df1 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 25 Apr 2024 08:43:37 -0700 Subject: [PATCH 59/69] remove unused test_utils --- tom_targets/tests/test_utils.py | 34 --------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 tom_targets/tests/test_utils.py diff --git a/tom_targets/tests/test_utils.py b/tom_targets/tests/test_utils.py deleted file mode 100644 index 8c28662db..000000000 --- a/tom_targets/tests/test_utils.py +++ /dev/null @@ -1,34 +0,0 @@ -from tom_targets.base_models import TargetMatchManager - - -class StrictMatch(TargetMatchManager): - """ - Return Queryset for target with name matching string. - """ - - def get_name_match(self, name): - """ - Returns a queryset exactly matching name that is received - :param name: The string against which target names will be matched. - :return: queryset containing matching Target(s). - """ - queryset = self.check_for_exact_name_match(name) - return queryset - - -class ConeSearchManager(TargetMatchManager): - """ - Return Queryset for target with name matching string. - """ - - def check_unique(self, target, *args, **kwargs): - """ - Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of - the target that is received - :param target: The target object to be checked. - :return: queryset containing matching Target(s). - """ - queryset = super().check_unique(target, *args, **kwargs) - radius = 2 - cone_search_queryset = self.check_for_nearby_match(target.ra, target.dec, radius) - return queryset | cone_search_queryset From 600f6613bb39bd26a8f44e2906c908257470df02 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 25 Apr 2024 15:42:49 -0700 Subject: [PATCH 60/69] update names and docs --- docs/targets/target_matcher.rst | 77 +++++++++++++++------ tom_dataproducts/alertstreams/hermes.py | 2 +- tom_setup/templates/tom_setup/settings.tmpl | 2 +- tom_targets/base_models.py | 73 ++++++++++++------- tom_targets/models.py | 2 +- tom_targets/tests/tests.py | 12 ++-- 6 files changed, 111 insertions(+), 57 deletions(-) diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index 4854c9711..a2b6d2154 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -24,7 +24,7 @@ You can use the ``TargetMatchManager`` to return a queryset of targets that sati radius = 12 # Arcseconds # Get the queryset of targets that match the cone search - targets = Target.matches.check_for_nearby_match(ra, dec, radius) + targets = Target.matches.cone_search(ra, dec, radius) Extending the TargetMatchManager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -44,13 +44,20 @@ To start, find the ``MATCH_MANAGERS`` definition in your ``settings.py``: Add the path to your custom ``TargetMatchManager`` to the "Target" key of the MATCH_MANAGERS dictionary as shown in the example. -The following code provides an example of a custom ``TargetMatchManager`` that checks for exact name matches and -requires that a target must have an RA and DEC more than 2" away from any other target in the database to be considered -unique: +Once you have defined your custom ``TargetMatchManager`` in ``settings.py``, you can need to create the custom +``TargetMatchManager`` in your project. We recommend you do this inside your project's ``custom_code`` app, but can be +placed anywhere. + +The ``TargetMatchManager`` can be extended to include additional methods or to override any of the default methods +described in :doc:`Target: Models <../api/tom_targets/models>`. The following code provides an example of a custom +``TargetMatchManager`` that checks for exact name matches instead of the default fuzzy matches. This would change the +default behavior for several parts of the TOM Toolkit that endeavor to determine if a target or alias is unique based on +its name. .. code-block:: python :caption: match_managers.py :linenos: + :emphasize-lines: 15 from tom_targets.base_models import TargetMatchManager @@ -60,32 +67,58 @@ unique: Custom Match Manager for extending the built in TargetMatchManager. """ - def check_unique(self, target, *args, **kwargs): + def name(self, name): + """ + Returns a queryset exactly matching name that is received + :param name: The string against which target names will be matched. + :return: queryset containing matching Target(s). + """ + queryset = self.exact_name(name) + return queryset + + +.. note:: + The default behavior for ``matches.name`` is to perform a "fuzzy match". This can be computationally expensive + for large databases. If you have experienced this issue, you can override the ``name`` method to only + return exact matches using the above example. + + +Next we have another example of a ``TargetMatchManager`` that extends the ``target`` matcher to not only include name +matches but also considers any target with an RA and DEC less than 2" away from the given target to be a match for the +target. + +.. code-block:: python + :caption: match_managers.py + :linenos: + :emphasize-lines: 17, 18 + + from tom_targets.base_models import TargetMatchManager + + + class CustomTargetMatchManager(TargetMatchManager): + """ + Custom Match Manager for extending the built in TargetMatchManager. + """ + + def target(self, target, *args, **kwargs): """ Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of the target that is received :param target: The target object to be checked. :return: queryset containing matching Target(s). """ - queryset = super().check_unique(target, *args, **kwargs) - radius = 2 - cone_search_queryset = self.check_for_nearby_match(target.ra, target.dec, radius) + queryset = super().target(target, *args, **kwargs) + radius = 2 # Arcseconds + cone_search_queryset = self.cone_search(target.ra, target.dec, radius) return queryset | cone_search_queryset - def get_name_match(self, name): - """ - Returns a queryset exactly matching name that is received - :param name: The string against which target names will be matched. - :return: queryset containing matching Target(s). - """ - queryset = self.check_for_exact_name_match(name) - return queryset -Your ``MatchManager`` should extend the ``base_model.TargetMatchManager`` which will contain both a ``check_unique`` -method and a ``get_name_match`` method, both of which should return a queryset. These methods can be modified or +The highlighted lines could be replaced with any custom logic that you would like to use to determine if a target in +the database is a match for the target that is being checked. This is extremely powerful since this code is ultimately used +by ``Target.validate_unique()`` to determine if a new target can be saved to the database, and thus prevent your TOM +from accidentally ingesting duplicate targets. + +Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` which will contain both a ``target`` +method and a ``name`` method, both of which should return a queryset. These methods can be modified or extended, as in the above example, as needed. -.. note:: - The default behavior for ``get_name_match`` is to perform a "fuzzy match". This can be computationally expensive - for large databases. If you have experienced this issue, you can override the ``get_name_match`` method to only - return exact matches using the above example. diff --git a/tom_dataproducts/alertstreams/hermes.py b/tom_dataproducts/alertstreams/hermes.py index e9e7dd6b0..5bfad9b7d 100644 --- a/tom_dataproducts/alertstreams/hermes.py +++ b/tom_dataproducts/alertstreams/hermes.py @@ -208,7 +208,7 @@ def hermes_alert_handler(alert, metadata): for row in photometry_table: if row['target_name'] != target_name: target_name = row['target_name'] - query = Target.matches.get_name_match(target_name) + query = Target.matches.name(target_name) if query: target = query[0] else: diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl index 9cfd80bd4..05db1df41 100644 --- a/tom_setup/templates/tom_setup/settings.tmpl +++ b/tom_setup/templates/tom_setup/settings.tmpl @@ -201,7 +201,7 @@ TARGET_TYPE = '{{ TARGET_TYPE }}' # for a given model. # For example: # MATCH_MANAGERS = { -# "Target": "custom_code.match_managers.MyCustomTargetMatchManager" +# "Target": "custom_code.match_managers.CustomTargetMatchManager" # } MATCH_MANAGERS = {} diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index ce0efbca3..aafbb7773 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -47,38 +47,59 @@ class TargetMatchManager(models.Manager): Return Queryset containing relevant TARGET matches. NOTE: - ``check_unique`` and ``get_name_match`` are used throughout the code to determine if a target or a name is + ``is_unique`` and ``name`` are used throughout the code to determine if a target or a name is unique. These functions can be overridden in a subclass to provide custom matching logic. Examples of this can be found in the documentation (https://tom-toolkit.readthedocs.io/en/stable/targets/target_matcher.html). """ - def check_unique(self, target, *args, **kwargs): + def is_unique(self, target, *args, **kwargs): """ - Check if any other targets match the given target. By default, this checks for a match in the name field using - the get_name_match function. - This function is used in the ``Target.validate_unique()`` function to check for uniqueness. + Check if the given target is unique. This function uses ``TargetMatchManager.target()`` to determine if any + targets exist in the DB other than the given target would be considered by the user to be a duplicate of the + given target. + + This function is used in the ``Target.validate_unique()`` function to check for uniqueness. + + :param target: The target object to be checked against. + + :return: True if the target is unique, False otherwise. + + """ + if self.target(target, *args, **kwargs).exclude(pk=target.pk).exists(): + return False + return True + + def target(self, target, *args, **kwargs): + """ + Check if any other targets match the given target. This function returns a queryset that is used by + ``TargetMatchManager.is_unique()`` to determine if a target is unique. + + By default, this checks for a match in the name field using the `name` function. + This can be overridden in a subclass to provide custom matching logic. :param target: The target object to be checked against. :return: queryset containing matching Target(s). """ - queryset = self.get_name_match(target.name) + queryset = self.name(target.name) return queryset - def get_name_match(self, name): + def name(self, name): """ - Returns a queryset of matching names. By default, this checks for a fuzzy match using the - ``check_for_fuzzy_name_match`` function. + Returns a queryset of targets with matching names. + + By default, this checks for a fuzzy match using the ``fuzzy_name`` function. + This can be overridden in a subclass to provide custom matching logic. :param name: The string against which target names will be matched. :return: queryset containing matching Target(s). """ - queryset = self.check_for_fuzzy_name_match(name) + queryset = self.fuzzy_name(name) return queryset - def check_for_nearby_match(self, ra: float, dec: float, radius: float): + def cone_search(self, ra: float, dec: float, radius: float): """ Returns a queryset containing any targets that are within the given radius of the given ra and dec. @@ -114,9 +135,9 @@ def check_for_nearby_match(self, ra: float, dec: float, radius: float): return queryset.annotate(separation=separation).filter(separation__lte=radius) - def check_for_exact_name_match(self, name): + def exact_name(self, name): """ - Returns a queryset exactly matching name that is received + Returns a queryset of targets with a name that exactly match the name that is received :param name: The string against which target names will be matched. @@ -125,27 +146,29 @@ def check_for_exact_name_match(self, name): queryset = super().get_queryset().filter(name=name) return queryset - def check_for_fuzzy_name_match(self, name): + def fuzzy_name(self, name): """ - Check for case-insensitive names ignoring spaces, dashes, underscore, and parentheses. + Returns a queryset of targets with a name OR ALIAS that, when processed by ``simplify_name``, match a similarly + processed version of the name that is received. :param name: The string against which target names and aliases will be matched. :return: queryset containing matching Targets. Will return targets even when matched value is an alias. """ - simple_name = self.make_simple_name(name) + simple_name = self.simple_name(name) matching_names = [] for target in self.get_queryset().all().prefetch_related('aliases'): for alias in target.names: - if self.make_simple_name(alias) == simple_name: + if self.simple_name(alias) == simple_name: matching_names.append(target.name) queryset = self.get_queryset().filter(name__in=matching_names) return queryset - def make_simple_name(self, name): + def simplify_name(self, name): """ - Create a simplified name to be used for comparison in check_for_fuzzy_name_match. + Create a simplified name to be used for comparison in ``fuzzy_name``. By default, this method removes capitalization, spaces, dashes, underscores, and parentheses from the name. + This can be overridden in a subclass to provide custom name simplification. :param name: The string to be simplified. @@ -414,18 +437,16 @@ def validate_unique(self, *args, **kwargs): super().validate_unique(*args, **kwargs) # Check DB for similar target/alias names. - matches = self.__class__.matches.check_unique(self) - for match in matches: - # Ignore the fact that this target's name matches itself. - if match.id != self.id: - raise ValidationError(f'A Target matching {self.name} already exists. ({match.name})') + if not self.__class__.matches.is_unique(self): + raise ValidationError(f'A Target matching {self.name} already exists. ' + f'({self.__class__.matches.target(self).exclude(id=self.id).first().name})') # Alias Check only necessary when updating target existing target. Reverse relationships require Primary Key. # If nothing has changed for the Target, do not validate against existing aliases. if self.pk and self.name != self.__class__.objects.get(pk=self.pk).name: for alias in self.aliases.all(): # Check for fuzzy matching - if self.__class__.matches.make_simple_name(alias.name) == \ - self.__class__.matches.make_simple_name(self.name): + if self.__class__.matches.simple_name(alias.name) == \ + self.__class__.matches.simple_name(self.name): raise ValidationError('Target name and target aliases must be different') def __str__(self): diff --git a/tom_targets/models.py b/tom_targets/models.py index 1c748453f..b616341a7 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -75,7 +75,7 @@ def validate_unique(self, *args, **kwargs): f'(target_id={self.target.id})') # Check DB for similar target/alias names. - matches = Target.matches.get_name_match(self.name) + matches = Target.matches.name(self.name) if matches: raise ValidationError(f'Target with Name or alias similar to {self.name} already exists.' f' ({matches.first().name})') diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index af4b21468..96f87935e 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -890,8 +890,8 @@ def setUp(self): def test_strict_matching(self): fuzzy_name = "test_target" - fuzzy_matches = Target.matches.check_for_fuzzy_name_match(fuzzy_name) - strict_matches = Target.matches.check_for_exact_name_match(fuzzy_name) + fuzzy_matches = Target.matches.fuzzy_name(fuzzy_name) + strict_matches = Target.matches.exact_name(fuzzy_name) self.assertTrue(fuzzy_matches.exists()) self.assertFalse(strict_matches.exists()) @@ -900,18 +900,18 @@ def test_cone_search_matching(self): dec = -22.1 radius = 1 # Test for exact match - matches = Target.matches.check_for_nearby_match(ra, dec, radius) + matches = Target.matches.cone_search(ra, dec, radius) self.assertTrue(matches.exists()) # Test for slightly off match ra += 0.01 - matches = Target.matches.check_for_nearby_match(ra, dec, radius) + matches = Target.matches.cone_search(ra, dec, radius) self.assertFalse(matches.exists()) # Test for match with larger radius radius += 100 - matches = Target.matches.check_for_nearby_match(ra, dec, radius) + matches = Target.matches.cone_search(ra, dec, radius) self.assertTrue(matches.exists()) # Test for moving objects with no RA/DEC - matches = Target.matches.check_for_nearby_match(None, None, radius) + matches = Target.matches.cone_search(None, None, radius) self.assertFalse(matches.exists()) From 74b8f7b713c9b745b386ce13120d3c10c5976a24 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 25 Apr 2024 15:58:38 -0700 Subject: [PATCH 61/69] change names again... --- docs/targets/target_matcher.rst | 22 +++++++------- tom_dataproducts/alertstreams/hermes.py | 2 +- tom_targets/base_models.py | 40 ++++++++++++------------- tom_targets/models.py | 2 +- tom_targets/tests/tests.py | 12 ++++---- 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index a2b6d2154..935c65427 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -24,7 +24,7 @@ You can use the ``TargetMatchManager`` to return a queryset of targets that sati radius = 12 # Arcseconds # Get the queryset of targets that match the cone search - targets = Target.matches.cone_search(ra, dec, radius) + targets = Target.matches.match_cone_search(ra, dec, radius) Extending the TargetMatchManager ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -67,23 +67,23 @@ its name. Custom Match Manager for extending the built in TargetMatchManager. """ - def name(self, name): + def match_name(self, name): """ Returns a queryset exactly matching name that is received :param name: The string against which target names will be matched. :return: queryset containing matching Target(s). """ - queryset = self.exact_name(name) + queryset = self.match_exact_name(name) return queryset .. note:: - The default behavior for ``matches.name`` is to perform a "fuzzy match". This can be computationally expensive - for large databases. If you have experienced this issue, you can override the ``name`` method to only + The default behavior for ``match_name`` is to perform a "fuzzy match". This can be computationally expensive + for large databases. If you have experienced this issue, you can override the ``match_name`` method to only return exact matches using the above example. -Next we have another example of a ``TargetMatchManager`` that extends the ``target`` matcher to not only include name +Next we have another example of a ``TargetMatchManager`` that extends the ``match_target`` matcher to not only include name matches but also considers any target with an RA and DEC less than 2" away from the given target to be a match for the target. @@ -100,16 +100,16 @@ target. Custom Match Manager for extending the built in TargetMatchManager. """ - def target(self, target, *args, **kwargs): + def match_target(self, target, *args, **kwargs): """ Returns a queryset containing any targets that are both a fuzzy match and within 2 arcsec of the target that is received :param target: The target object to be checked. :return: queryset containing matching Target(s). """ - queryset = super().target(target, *args, **kwargs) + queryset = super().match_target(target, *args, **kwargs) radius = 2 # Arcseconds - cone_search_queryset = self.cone_search(target.ra, target.dec, radius) + cone_search_queryset = self.match_cone_search(target.ra, target.dec, radius) return queryset | cone_search_queryset @@ -118,7 +118,7 @@ the database is a match for the target that is being checked. This is extremely by ``Target.validate_unique()`` to determine if a new target can be saved to the database, and thus prevent your TOM from accidentally ingesting duplicate targets. -Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` which will contain both a ``target`` -method and a ``name`` method, both of which should return a queryset. These methods can be modified or +Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` which will contain both a ``match_target`` +method and a ``match_name`` method, both of which should return a queryset. These methods can be modified or extended, as in the above example, as needed. diff --git a/tom_dataproducts/alertstreams/hermes.py b/tom_dataproducts/alertstreams/hermes.py index 5bfad9b7d..ee7ab0e16 100644 --- a/tom_dataproducts/alertstreams/hermes.py +++ b/tom_dataproducts/alertstreams/hermes.py @@ -208,7 +208,7 @@ def hermes_alert_handler(alert, metadata): for row in photometry_table: if row['target_name'] != target_name: target_name = row['target_name'] - query = Target.matches.name(target_name) + query = Target.matches.match_name(target_name) if query: target = query[0] else: diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index aafbb7773..b959c09ee 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -47,16 +47,16 @@ class TargetMatchManager(models.Manager): Return Queryset containing relevant TARGET matches. NOTE: - ``is_unique`` and ``name`` are used throughout the code to determine if a target or a name is + ``is_unique`` and ``match_name`` are used throughout the code to determine if a target or a name is unique. These functions can be overridden in a subclass to provide custom matching logic. Examples of this can be found in the documentation (https://tom-toolkit.readthedocs.io/en/stable/targets/target_matcher.html). """ def is_unique(self, target, *args, **kwargs): """ - Check if the given target is unique. This function uses ``TargetMatchManager.target()`` to determine if any - targets exist in the DB other than the given target would be considered by the user to be a duplicate of the - given target. + Check if the given target is unique. This function uses ``TargetMatchManager.match_target()`` to determine if + any targets exist in the DB other than the given target would be considered by the user to be a duplicate of + the given target. This function is used in the ``Target.validate_unique()`` function to check for uniqueness. @@ -65,16 +65,16 @@ def is_unique(self, target, *args, **kwargs): :return: True if the target is unique, False otherwise. """ - if self.target(target, *args, **kwargs).exclude(pk=target.pk).exists(): + if self.match_target(target, *args, **kwargs).exclude(pk=target.pk).exists(): return False return True - def target(self, target, *args, **kwargs): + def match_target(self, target, *args, **kwargs): """ Check if any other targets match the given target. This function returns a queryset that is used by ``TargetMatchManager.is_unique()`` to determine if a target is unique. - By default, this checks for a match in the name field using the `name` function. + By default, this checks for a match in the name field using the `match_name` function. This can be overridden in a subclass to provide custom matching logic. :param target: The target object to be checked against. @@ -82,24 +82,24 @@ def target(self, target, *args, **kwargs): :return: queryset containing matching Target(s). """ - queryset = self.name(target.name) + queryset = self.match_name(target.name) return queryset - def name(self, name): + def match_name(self, name): """ Returns a queryset of targets with matching names. - By default, this checks for a fuzzy match using the ``fuzzy_name`` function. + By default, this checks for a fuzzy match using the ``match_fuzzy_name`` function. This can be overridden in a subclass to provide custom matching logic. :param name: The string against which target names will be matched. :return: queryset containing matching Target(s). """ - queryset = self.fuzzy_name(name) + queryset = self.match_fuzzy_name(name) return queryset - def cone_search(self, ra: float, dec: float, radius: float): + def match_cone_search(self, ra: float, dec: float, radius: float): """ Returns a queryset containing any targets that are within the given radius of the given ra and dec. @@ -135,7 +135,7 @@ def cone_search(self, ra: float, dec: float, radius: float): return queryset.annotate(separation=separation).filter(separation__lte=radius) - def exact_name(self, name): + def match_exact_name(self, name): """ Returns a queryset of targets with a name that exactly match the name that is received @@ -146,7 +146,7 @@ def exact_name(self, name): queryset = super().get_queryset().filter(name=name) return queryset - def fuzzy_name(self, name): + def match_fuzzy_name(self, name): """ Returns a queryset of targets with a name OR ALIAS that, when processed by ``simplify_name``, match a similarly processed version of the name that is received. @@ -155,18 +155,18 @@ def fuzzy_name(self, name): :return: queryset containing matching Targets. Will return targets even when matched value is an alias. """ - simple_name = self.simple_name(name) + simple_name = self.simplify_name(name) matching_names = [] for target in self.get_queryset().all().prefetch_related('aliases'): for alias in target.names: - if self.simple_name(alias) == simple_name: + if self.simplify_name(alias) == simple_name: matching_names.append(target.name) queryset = self.get_queryset().filter(name__in=matching_names) return queryset def simplify_name(self, name): """ - Create a simplified name to be used for comparison in ``fuzzy_name``. + Create a simplified name to be used for comparison in ``match_fuzzy_name``. By default, this method removes capitalization, spaces, dashes, underscores, and parentheses from the name. This can be overridden in a subclass to provide custom name simplification. @@ -439,14 +439,14 @@ def validate_unique(self, *args, **kwargs): if not self.__class__.matches.is_unique(self): raise ValidationError(f'A Target matching {self.name} already exists. ' - f'({self.__class__.matches.target(self).exclude(id=self.id).first().name})') + f'({self.__class__.matches.match_target(self).exclude(id=self.id).first().name})') # Alias Check only necessary when updating target existing target. Reverse relationships require Primary Key. # If nothing has changed for the Target, do not validate against existing aliases. if self.pk and self.name != self.__class__.objects.get(pk=self.pk).name: for alias in self.aliases.all(): # Check for fuzzy matching - if self.__class__.matches.simple_name(alias.name) == \ - self.__class__.matches.simple_name(self.name): + if self.__class__.matches.simplify_name(alias.name) == \ + self.__class__.matches.simplify_name(self.name): raise ValidationError('Target name and target aliases must be different') def __str__(self): diff --git a/tom_targets/models.py b/tom_targets/models.py index b616341a7..49fa4404e 100644 --- a/tom_targets/models.py +++ b/tom_targets/models.py @@ -75,7 +75,7 @@ def validate_unique(self, *args, **kwargs): f'(target_id={self.target.id})') # Check DB for similar target/alias names. - matches = Target.matches.name(self.name) + matches = Target.matches.match_name(self.name) if matches: raise ValidationError(f'Target with Name or alias similar to {self.name} already exists.' f' ({matches.first().name})') diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index 96f87935e..72f77b661 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -890,8 +890,8 @@ def setUp(self): def test_strict_matching(self): fuzzy_name = "test_target" - fuzzy_matches = Target.matches.fuzzy_name(fuzzy_name) - strict_matches = Target.matches.exact_name(fuzzy_name) + fuzzy_matches = Target.matches.match_fuzzy_name(fuzzy_name) + strict_matches = Target.matches.match_exact_name(fuzzy_name) self.assertTrue(fuzzy_matches.exists()) self.assertFalse(strict_matches.exists()) @@ -900,18 +900,18 @@ def test_cone_search_matching(self): dec = -22.1 radius = 1 # Test for exact match - matches = Target.matches.cone_search(ra, dec, radius) + matches = Target.matches.match_cone_search(ra, dec, radius) self.assertTrue(matches.exists()) # Test for slightly off match ra += 0.01 - matches = Target.matches.cone_search(ra, dec, radius) + matches = Target.matches.match_cone_search(ra, dec, radius) self.assertFalse(matches.exists()) # Test for match with larger radius radius += 100 - matches = Target.matches.cone_search(ra, dec, radius) + matches = Target.matches.match_cone_search(ra, dec, radius) self.assertTrue(matches.exists()) # Test for moving objects with no RA/DEC - matches = Target.matches.cone_search(None, None, radius) + matches = Target.matches.match_cone_search(None, None, radius) self.assertFalse(matches.exists()) From 62115676a2bdef5fb07e8ba1bca84ade4f856255 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Thu, 25 Apr 2024 17:04:36 -0700 Subject: [PATCH 62/69] add simplify name docs --- docs/targets/target_matcher.rst | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index 935c65427..e286124ca 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -122,3 +122,43 @@ Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` whic method and a ``match_name`` method, both of which should return a queryset. These methods can be modified or extended, as in the above example, as needed. +Customizing ``match_fuzzy_name`` +++++++++++++++++++++++++++++++++ + +The ``match_fuzzy_name`` method is used to query the database for targets whose names ~kind of~ match the given string. +This method relies on ``simplify_name`` to create a processed version of the input string that can be compared to +similarly processed names and aliases in the database. By default, ``simplify_name`` removes capitalization, spaces, +dashes, underscores, and parentheses from the names, thus ``match_fuzzy_name`` will return targets whose names match +the given string ignoring these characters. (i.e. "My Target" will match both "my_target" and "(mY)tAr-GeT"). + +If you would like to customize the behavior of ``match_fuzzy_name``, you can override the ``simplify_name`` method in +your custom ``TargetMatchManager``. The following example demonstrates how to extend ``simplify_name`` to also consider +two names to be a match if they start with either 'AT' or 'SN'. + + +.. code-block:: python + :caption: match_managers.py + :linenos: + :emphasize-lines: 14, 15 + + from tom_targets.base_models import TargetMatchManager + + + class CustomTargetMatchManager(TargetMatchManager): + """ + Custom Match Manager for extending the built in TargetMatchManager. + """ + + def simplify_name(self, name): + """ + Create a custom simplified name to be used for comparison in ``match_fuzzy_name``. + """ + simple_name = super().simplify_name(name) # Use the default simplification + if simple_name.startswith('at'): + simple_name = simple_name.replace('at', 'sn', 1) + return simple_name + + +The highlighted lines could be replaced with any custom logic that you would like to use to determine if a target in +the database is a match for the name that is being checked. *NOTE* this will only actually be used by +``match_fuzzy_name``. If you are using ``match_exact_name`` these changes will not be used. \ No newline at end of file From 9cf8552277d98acccd62824eed547d3a96276563 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Mon, 20 May 2024 15:33:00 -0700 Subject: [PATCH 63/69] fix cone_matcher --- tom_targets/base_models.py | 22 +++++++++++++++++----- tom_targets/tests/tests.py | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index b959c09ee..0a603aa63 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError from django.db import models, transaction from django.db.models.functions.math import ACos, Cos, Radians, Pi, Sin +from django.db.models.functions import Least from django.forms.models import model_to_dict from django.urls import reverse from django.utils.module_loading import import_string @@ -116,21 +117,32 @@ def match_cone_search(self, ra: float, dec: float, radius: float): """ # Return an empty queryset if any of the parameters are None such as for a NonSidereal target - if ra is None or dec is None or radius is None: + # Return an empty queryset if the dec is outside the range -90 to 90 + if ra is None or dec is None or radius is None or dec < -90 or dec > 90: return self.get_queryset().none() + # Ensure that the search ra is between 0 and 360 + ra %= 360 + radius /= 3600 # Convert radius from arcseconds to degrees double_radius = radius * 2 + # Perform initial filter to reduce the number of targets that need a calculated separation queryset = super().get_queryset().filter( ra__gte=ra - double_radius, ra__lte=ra + double_radius, dec__gte=dec - double_radius, dec__lte=dec + double_radius ) + # Calculate the angular separation between the target and the given ra and dec + # Uses Django Database Functions, to perform the calculation in the database. + # Includes a "Least" function to ensure that the value passed to the ACos function is never greater than 1 + # due to floating point errors. separation = models.ExpressionWrapper( - 180 * ACos( - (Sin(radians(dec)) * Sin(Radians('dec'))) + - (Cos(radians(dec)) * Cos(Radians('dec')) * Cos(radians(ra) - Radians('ra'))) - ) / Pi(), models.FloatField() + ACos( + Least( + (Sin(radians(dec)) * Sin(Radians('dec'))) + + (Cos(radians(dec)) * Cos(Radians('dec')) * Cos(radians(ra) - Radians('ra'))), 1.0 + ) + ) * 180 / Pi(), models.FloatField() ) return queryset.annotate(separation=separation).filter(separation__lte=radius) diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py index 72f77b661..c8971eb4e 100644 --- a/tom_targets/tests/tests.py +++ b/tom_targets/tests/tests.py @@ -914,6 +914,31 @@ def test_cone_search_matching(self): matches = Target.matches.match_cone_search(None, None, radius) self.assertFalse(matches.exists()) + def test_unreasonable_cone_search_matching(self): + ra = self.target.ra + dec = self.target.dec + radius = 1 + # Test for exact match + matches = Target.matches.match_cone_search(ra, dec, radius) + self.assertTrue(matches.exists()) + ra += 360 + # Test for high RA match + matches = Target.matches.match_cone_search(ra, dec, radius) + self.assertTrue(matches.exists()) + ra -= 360 * 3 + # Test for low RA match + matches = Target.matches.match_cone_search(ra, dec, radius) + self.assertTrue(matches.exists()) + dec = 95 + radius = 360 * 3600 # Cover entire sky + # Test for no too high DEC match + matches = Target.matches.match_cone_search(ra, dec, radius) + self.assertFalse(matches.exists()) + dec = -95 + # Test for no too low DEC match + matches = Target.matches.match_cone_search(ra, dec, radius) + self.assertFalse(matches.exists()) + class TestTargetImport(TestCase): def setUp(self): From c6d3105acdbb552412e3dee72a75cbf252430481 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Tue, 21 May 2024 11:38:36 -0700 Subject: [PATCH 64/69] add a little detail comment --- tom_targets/base_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tom_targets/base_models.py b/tom_targets/base_models.py index 0a603aa63..1afb415be 100644 --- a/tom_targets/base_models.py +++ b/tom_targets/base_models.py @@ -135,7 +135,8 @@ def match_cone_search(self, ra: float, dec: float, radius: float): # Calculate the angular separation between the target and the given ra and dec # Uses Django Database Functions, to perform the calculation in the database. # Includes a "Least" function to ensure that the value passed to the ACos function is never greater than 1 - # due to floating point errors. + # due to floating point errors. We ignore the case of this being less than -1 since this will only happen when + # the target is on the opposite side of the sky from the search coordinates. separation = models.ExpressionWrapper( ACos( Least( From a5e112299bfc87a76e66e2a950ecb46eb94b6274 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 29 May 2024 15:07:04 -0700 Subject: [PATCH 65/69] overwrite default values when converting target extras. --- tom_base/settings.py | 2 +- .../management/commands/converttargetextras.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tom_base/settings.py b/tom_base/settings.py index 59325d553..233679a5e 100644 --- a/tom_base/settings.py +++ b/tom_base/settings.py @@ -322,6 +322,6 @@ } try: - from local_settings import * # noqa + from .local_settings import * # noqa except ImportError: pass diff --git a/tom_targets/management/commands/converttargetextras.py b/tom_targets/management/commands/converttargetextras.py index aeedffddc..b958d2972 100644 --- a/tom_targets/management/commands/converttargetextras.py +++ b/tom_targets/management/commands/converttargetextras.py @@ -44,6 +44,12 @@ def add_arguments(self, parser): action='store_true', help='Provide a list of available TargetExtras and Model Fields.', ) + parser.add_argument( + '--force', + action='store_true', + help='Overwrite any existing values in the relevant model fields with those from the corresponding ' + 'TargetExtra.', + ) def prompt_extra_field(self, extra_field_keys): """ @@ -97,7 +103,7 @@ def confirm_conversion(self, chosen_extra, chosen_model_field): else: self.stdout.write('Invalid response. Please try again.') - def convert_target_extra(self, chosen_extra, chosen_model_field): + def convert_target_extra(self, chosen_extra, chosen_model_field, force=False): """ Perform the actual conversion from a `chosen_extra` to a `chosen_model_field` for each target that has one of these TargetExtras. @@ -107,7 +113,10 @@ def convert_target_extra(self, chosen_extra, chosen_model_field): """ for extra in TargetExtra.objects.filter(key=chosen_extra): target = Target.objects.get(pk=extra.target.pk) - if getattr(target, chosen_model_field, None): + model_field_default = Target._meta.get_field(chosen_model_field).get_default() + # If model already has a value, don't overwrite unless it's the default value or force is True + if not force and getattr(target, chosen_model_field, None) \ + and getattr(target, chosen_model_field) != model_field_default: self.stdout.write(f"{self.style.ERROR('Warning:')} {target}.{chosen_model_field} " f"already has a value: {getattr(target, chosen_model_field)}. Skipping.") continue @@ -164,6 +173,6 @@ def handle(self, *args, **options): if not confirmed: continue - self.convert_target_extra(chosen_extra, chosen_model_field) + self.convert_target_extra(chosen_extra, chosen_model_field, options['force']) return From 81468fb917912d338990b165bdd7fc7e92d6344d Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 29 May 2024 15:10:21 -0700 Subject: [PATCH 66/69] update docs --- docs/targets/target_fields.rst | 3 ++- tom_base/settings.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/targets/target_fields.rst b/docs/targets/target_fields.rst index ea9b9704a..7a16865a7 100644 --- a/docs/targets/target_fields.rst +++ b/docs/targets/target_fields.rst @@ -228,7 +228,8 @@ more the of the ``Extra Field`` and ``Target Field`` names respectively. ./manage.py converttargetextras --target_extra extra_bool extra_number --model_field example_bool example_number This command will go through each target and transfer the data from the ``Extra Field`` to the ``Target Field``. If the -``Target Field`` is already populated, the data will not be transferred. When finished, the ``Extra Field`` data will be +``Target Field`` is already populated with a value other than the default value, the data will not be transferred unless +the ``--force`` flag is set. When finished, the ``Extra Field`` data will be deleted, and you will likely want to remove the ``EXTRA_FIELDS`` setting from your ``settings.py`` file. Adding ``Extra Fields`` diff --git a/tom_base/settings.py b/tom_base/settings.py index 233679a5e..59325d553 100644 --- a/tom_base/settings.py +++ b/tom_base/settings.py @@ -322,6 +322,6 @@ } try: - from .local_settings import * # noqa + from local_settings import * # noqa except ImportError: pass From d8c28132ee341b603fcafa3678ce12a9df4e8bf3 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 31 May 2024 10:25:22 -0700 Subject: [PATCH 67/69] reformat long conditional --- tom_targets/management/commands/converttargetextras.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tom_targets/management/commands/converttargetextras.py b/tom_targets/management/commands/converttargetextras.py index b958d2972..db32acc69 100644 --- a/tom_targets/management/commands/converttargetextras.py +++ b/tom_targets/management/commands/converttargetextras.py @@ -115,8 +115,9 @@ def convert_target_extra(self, chosen_extra, chosen_model_field, force=False): target = Target.objects.get(pk=extra.target.pk) model_field_default = Target._meta.get_field(chosen_model_field).get_default() # If model already has a value, don't overwrite unless it's the default value or force is True - if not force and getattr(target, chosen_model_field, None) \ - and getattr(target, chosen_model_field) != model_field_default: + if not force and \ + getattr(target, chosen_model_field, None) and \ + getattr(target, chosen_model_field) != model_field_default: self.stdout.write(f"{self.style.ERROR('Warning:')} {target}.{chosen_model_field} " f"already has a value: {getattr(target, chosen_model_field)}. Skipping.") continue From 2b65a08091c65856790c775b01768bbf39380e9f Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Wed, 26 Jun 2024 16:52:33 -0700 Subject: [PATCH 68/69] add warning about using save and create methods --- docs/targets/target_matcher.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index e286124ca..172daa3c5 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -118,6 +118,13 @@ the database is a match for the target that is being checked. This is extremely by ``Target.validate_unique()`` to determine if a new target can be saved to the database, and thus prevent your TOM from accidentally ingesting duplicate targets. +.. warning:: + The `validate_unique()` method is not called when using the `.save()` or `.create()` methods on a model. If you are + creating targets in your TOM's custom code, you should call `validate_unique()` manually to ensure that the target + is unique, or the `full_clean()` method to make sure that all of the individual fields are valid as well. See the + `Django Docs `__ + for more information. + Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` which will contain both a ``match_target`` method and a ``match_name`` method, both of which should return a queryset. These methods can be modified or extended, as in the above example, as needed. From 397a230de407b50d567ec9ee8cf3360e49b5c1d2 Mon Sep 17 00:00:00 2001 From: Joey Chatelain Date: Fri, 28 Jun 2024 18:23:09 -0700 Subject: [PATCH 69/69] update docs on saving and validating targets --- docs/targets/target_matcher.rst | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/targets/target_matcher.rst b/docs/targets/target_matcher.rst index 172daa3c5..5b7d910bd 100644 --- a/docs/targets/target_matcher.rst +++ b/docs/targets/target_matcher.rst @@ -118,17 +118,35 @@ the database is a match for the target that is being checked. This is extremely by ``Target.validate_unique()`` to determine if a new target can be saved to the database, and thus prevent your TOM from accidentally ingesting duplicate targets. -.. warning:: - The `validate_unique()` method is not called when using the `.save()` or `.create()` methods on a model. If you are - creating targets in your TOM's custom code, you should call `validate_unique()` manually to ensure that the target - is unique, or the `full_clean()` method to make sure that all of the individual fields are valid as well. See the - `Django Docs `__ - for more information. - Your ``MatchManager`` should subclass the ``base_model.TargetMatchManager`` which will contain both a ``match_target`` method and a ``match_name`` method, both of which should return a queryset. These methods can be modified or extended, as in the above example, as needed. +A Note About Saving Targets: +++++++++++++++++++++++++++++ + +The `Target.validate_unique()` method is not called when using the `Target.save()` or `Target.objects.create()` +methods to save a model. If you are creating targets in your TOM's custom code, you should call `validate_unique()` +manually to ensure that the target is unique, or use the `full_clean()` method to make sure that all of the individual +fields are valid as well. See the +`Django Docs `__ +for more information. + +If you do wish to use your new match manager to validate or updated targets your code should look something like this: + +.. code-block:: python + :linenos: + + from django.core.exceptions import ValidationError + from tom_targets.models import Target + + target = Target(name='My Target', ra=10.68458, dec=41.26906) + try: + target.validate_unique() # or `target.full_clean()` + target.save() + except ValidationError as e: + print(f'{target.name} not saved: {e}') + Customizing ``match_fuzzy_name`` ++++++++++++++++++++++++++++++++