localcosmos-app-kit 0.9.17__py3-none-any.whl → 0.9.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- app_kit/taxonomy/sources/custom/management/commands/__pycache__/import_custom_species_csv.cpython-313.pyc +0 -0
- app_kit/taxonomy/sources/custom/management/commands/__pycache__/import_custom_taxonomy_csv.cpython-313.pyc +0 -0
- app_kit/taxonomy/sources/custom/management/commands/import_custom_species_csv.py +177 -0
- app_kit/taxonomy/sources/custom/management/commands/import_custom_taxonomy_csv.py +177 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.9.18.dist-info}/METADATA +1 -1
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.9.18.dist-info}/RECORD +9 -5
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.9.18.dist-info}/WHEEL +0 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.9.18.dist-info}/licenses/LICENCE +0 -0
- {localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.9.18.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
6
|
+
from django.db import transaction
|
|
7
|
+
|
|
8
|
+
from taxonomy.models import TaxonomyModelRouter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
help = (
|
|
13
|
+
'Importiert die Arten-/Taxa-CSV (|-getrennt). Erkennt Headerzeile automatisch und überspringt sie.\n'
|
|
14
|
+
'Unterstütztes Layout (aktuell):\n'
|
|
15
|
+
' 0: scientific_name | 1: rank | 2: parent_lat | 3: parent_code | 4: de | 5: en | 6: nl | 7: da'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def add_arguments(self, parser):
|
|
19
|
+
parser.add_argument('csv_path', type=str, help='Pfad zur Arten-CSV (|-getrennt).')
|
|
20
|
+
parser.add_argument('--delimiter', type=str, default='|', help='Trennzeichen (Standard: |).')
|
|
21
|
+
parser.add_argument('--encoding', type=str, default='utf-8', help='Datei-Kodierung (Standard: utf-8).')
|
|
22
|
+
parser.add_argument('--dry-run', action='store_true', help='Nur prüfen, am Ende zurückrollen.')
|
|
23
|
+
|
|
24
|
+
def handle(self, *args, **options):
|
|
25
|
+
csv_path = options['csv_path']
|
|
26
|
+
delimiter = options['delimiter'] or '|'
|
|
27
|
+
encoding = options['encoding']
|
|
28
|
+
dry_run = options['dry_run']
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(csv_path):
|
|
31
|
+
raise CommandError(f'Datei nicht gefunden: {csv_path}')
|
|
32
|
+
|
|
33
|
+
models = TaxonomyModelRouter('taxonomy.sources.custom')
|
|
34
|
+
self.stdout.write(self.style.NOTICE('Quelle: taxonomy.sources.custom'))
|
|
35
|
+
self.stdout.write(self.style.NOTICE(f'Trennzeichen: {repr(delimiter)}'))
|
|
36
|
+
|
|
37
|
+
created = 0
|
|
38
|
+
skipped = 0
|
|
39
|
+
existing = 0
|
|
40
|
+
rows = 0
|
|
41
|
+
self._missing_parents = {}
|
|
42
|
+
self._header_skipped = False
|
|
43
|
+
self._existing_names = []
|
|
44
|
+
|
|
45
|
+
with open(csv_path, 'r', encoding=encoding, newline='') as f:
|
|
46
|
+
reader = csv.reader(f, delimiter=delimiter)
|
|
47
|
+
|
|
48
|
+
with transaction.atomic():
|
|
49
|
+
for parts in reader:
|
|
50
|
+
rows += 1
|
|
51
|
+
if not parts:
|
|
52
|
+
continue
|
|
53
|
+
# Skip header line if present
|
|
54
|
+
if not self._header_skipped and self._looks_like_header(parts):
|
|
55
|
+
self._header_skipped = True
|
|
56
|
+
continue
|
|
57
|
+
c, e, s, existing_name = self._import_species_row(models, parts)
|
|
58
|
+
created += c
|
|
59
|
+
existing += e
|
|
60
|
+
skipped += s
|
|
61
|
+
if e and existing_name:
|
|
62
|
+
# preserve insertion order, avoid duplicates
|
|
63
|
+
if existing_name not in self._existing_names:
|
|
64
|
+
self._existing_names.append(existing_name)
|
|
65
|
+
|
|
66
|
+
if dry_run:
|
|
67
|
+
# Rollback alle DB-Änderungen, aber dennoch Auswertung anzeigen
|
|
68
|
+
transaction.set_rollback(True)
|
|
69
|
+
|
|
70
|
+
self.stdout.write(self.style.SUCCESS(
|
|
71
|
+
f'Fertig. Erstellt: {created}, vorhanden: {existing}, übersprungen: {skipped}, Zeilen: {rows}'))
|
|
72
|
+
if dry_run:
|
|
73
|
+
self.stdout.write(self.style.WARNING('Dry-Run: Alle Änderungen wurden zurückgerollt.'))
|
|
74
|
+
|
|
75
|
+
if existing and self._existing_names:
|
|
76
|
+
self.stdout.write(self.style.NOTICE('Bereits vorhandene Taxa (exakt):'))
|
|
77
|
+
for nm in self._existing_names:
|
|
78
|
+
self.stdout.write(f'- {nm}')
|
|
79
|
+
|
|
80
|
+
if skipped and self._missing_parents:
|
|
81
|
+
self.stdout.write(self.style.WARNING('Übersprungene Zeilen wegen fehlendem Eltern-Taxon (Auszug):'))
|
|
82
|
+
# Zeige bis zu 25 fehlende Eltern mit Häufigkeit
|
|
83
|
+
shown = 0
|
|
84
|
+
for parent_name, count in sorted(self._missing_parents.items(), key=lambda x: -x[1]):
|
|
85
|
+
self.stdout.write(f'- {parent_name or "<leer>"}: {count}')
|
|
86
|
+
shown += 1
|
|
87
|
+
if shown >= 25:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
def _looks_like_header(self, parts: List[str]) -> bool:
|
|
91
|
+
if not parts:
|
|
92
|
+
return False
|
|
93
|
+
p0 = (parts[0] or '').strip().lower()
|
|
94
|
+
p1 = (parts[1] or '').strip().lower() if len(parts) > 1 else ''
|
|
95
|
+
# Typical header tokens
|
|
96
|
+
header_tokens = {'scientific name', 'name', 'latname', 'parent taxon', 'rank', 'de', 'en', 'nl', 'dk'}
|
|
97
|
+
return (p0 in header_tokens) or (p1 in header_tokens)
|
|
98
|
+
|
|
99
|
+
def _get_parent(self, models, parent_latname: str):
|
|
100
|
+
if not parent_latname:
|
|
101
|
+
return None
|
|
102
|
+
# exakter Treffer zuerst
|
|
103
|
+
parent = models.TaxonTreeModel.objects.filter(taxon_latname=parent_latname).first()
|
|
104
|
+
if parent:
|
|
105
|
+
return parent
|
|
106
|
+
# Fallback: case-insensitive Vergleich
|
|
107
|
+
return models.TaxonTreeModel.objects.filter(taxon_latname__iexact=parent_latname).first()
|
|
108
|
+
|
|
109
|
+
def _get_or_create_taxon(self, models, parent, latname: str, rank: str):
|
|
110
|
+
if not latname:
|
|
111
|
+
return None, False
|
|
112
|
+
|
|
113
|
+
r = (rank or 'species').strip().lower()
|
|
114
|
+
qs = models.TaxonTreeModel.objects.filter(taxon_latname=latname, rank=r)
|
|
115
|
+
if parent is None:
|
|
116
|
+
qs = qs.filter(is_root_taxon=True)
|
|
117
|
+
else:
|
|
118
|
+
qs = qs.filter(parent=parent)
|
|
119
|
+
|
|
120
|
+
instance = qs.first()
|
|
121
|
+
created = False
|
|
122
|
+
if not instance:
|
|
123
|
+
extra = {'rank': r}
|
|
124
|
+
if parent is None:
|
|
125
|
+
extra['is_root_taxon'] = True
|
|
126
|
+
else:
|
|
127
|
+
extra['parent'] = parent
|
|
128
|
+
instance = models.TaxonTreeModel.objects.create(latname, None, **extra)
|
|
129
|
+
created = True
|
|
130
|
+
return instance, created
|
|
131
|
+
|
|
132
|
+
def _add_locale_if_present(self, models, taxon, name: Optional[str], language: str):
|
|
133
|
+
if not taxon or not name:
|
|
134
|
+
return False
|
|
135
|
+
exists = models.TaxonLocaleModel.objects.filter(taxon=taxon, name=name, language=language).exists()
|
|
136
|
+
if not exists:
|
|
137
|
+
models.TaxonLocaleModel.objects.create(taxon, name, language, preferred=True)
|
|
138
|
+
return True
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def _import_species_row(self, models, parts: List[str]):
|
|
142
|
+
# Unterstütztes Layout (aktuell):
|
|
143
|
+
# 0: scientific_name | 1: rank | 2: parent_lat | 3: parent_code | 4: de | 5: en | 6: nl | 7: da
|
|
144
|
+
|
|
145
|
+
# Normalize parts
|
|
146
|
+
parts = [(p or '').strip() for p in parts]
|
|
147
|
+
|
|
148
|
+
# Minimalprüfung: mindestens 4 Spalten (bis parent_code) erwartet
|
|
149
|
+
if len(parts) < 4:
|
|
150
|
+
return 0, 0, 1, None
|
|
151
|
+
|
|
152
|
+
name = parts[0]
|
|
153
|
+
rank = (parts[1] or 'species').strip().lower()
|
|
154
|
+
parent_latname = parts[2]
|
|
155
|
+
# parent_code aktuell informativ, optional verwendbar
|
|
156
|
+
# parent_code = parts[3]
|
|
157
|
+
locale_start_idx = 4
|
|
158
|
+
|
|
159
|
+
parent = self._get_parent(models, parent_latname) if parent_latname else None
|
|
160
|
+
# If no parent and this should be a root-level taxon, allow creation with no parent
|
|
161
|
+
if not parent and parent_latname:
|
|
162
|
+
key = parent_latname
|
|
163
|
+
self._missing_parents[key] = self._missing_parents.get(key, 0) + 1
|
|
164
|
+
return 0, 0, 1, None
|
|
165
|
+
|
|
166
|
+
taxon, created = self._get_or_create_taxon(models, parent, name, rank)
|
|
167
|
+
if not taxon:
|
|
168
|
+
return 0, 0, 1, None
|
|
169
|
+
|
|
170
|
+
# Attach vernacular locales if present (de, en, nl, da)
|
|
171
|
+
locale_langs = ['de', 'en', 'nl', 'da']
|
|
172
|
+
for offset, lang in enumerate(locale_langs):
|
|
173
|
+
idx = locale_start_idx + offset
|
|
174
|
+
val = parts[idx] if idx < len(parts) else ''
|
|
175
|
+
self._add_locale_if_present(models, taxon, val, lang)
|
|
176
|
+
|
|
177
|
+
return (1, 0, 0, None) if created else (0, 1, 0, name)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# temporary script for a one-time import of BeachExplorer's higher taxonom
|
|
2
|
+
import csv
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from django.core.management.base import BaseCommand, CommandError
|
|
8
|
+
from django.db import transaction
|
|
9
|
+
|
|
10
|
+
from taxonomy.models import TaxonomyModelRouter
|
|
11
|
+
|
|
12
|
+
RANK_SET = set([
|
|
13
|
+
'domain','kingdom','phylum','division','class','order','family','genus','species',
|
|
14
|
+
'subkingdom','infrakingdom','superphylum','subphylum','infraphylum',
|
|
15
|
+
'superclass','subclass','infraclass','superorder','suborder',
|
|
16
|
+
'infraorder','parvorder','superfamily','subfamily','tribe','subtribe',
|
|
17
|
+
'clade'
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_code_like(val: str) -> bool:
|
|
22
|
+
return bool(re.fullmatch(r'[a-z]{2,6}', val))
|
|
23
|
+
|
|
24
|
+
def _non_empty_indices(parts: List[str]) -> List[int]:
|
|
25
|
+
return [i for i, p in enumerate(parts) if p]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Command(BaseCommand):
|
|
29
|
+
help = 'Import a custom taxonomy tree from a GROK-Rang CSV (headerlos, |-getrennt).'
|
|
30
|
+
|
|
31
|
+
def add_arguments(self, parser):
|
|
32
|
+
parser.add_argument('csv_path', type=str, help='Pfad zur GROK-Rang CSV-Datei (ohne Header).')
|
|
33
|
+
parser.add_argument('--language', type=str, default=None,
|
|
34
|
+
help='Sprachcode für Bezeichnungen aus dem Pfad (z. B. de, en).')
|
|
35
|
+
parser.add_argument('--delimiter', type=str, default='|',
|
|
36
|
+
help='Trennzeichen (Standard: |).')
|
|
37
|
+
parser.add_argument('--encoding', type=str, default='utf-8',
|
|
38
|
+
help='Datei-Kodierung (Standard: utf-8).')
|
|
39
|
+
parser.add_argument('--dry-run', action='store_true', help='Nur prüfen, am Ende zurückrollen.')
|
|
40
|
+
parser.add_argument('--report-limit', type=int, default=10,
|
|
41
|
+
help='Maximale Anzahl Beispiel-Einträge in der Dry-Run-Übersicht (Standard: 10).')
|
|
42
|
+
|
|
43
|
+
def handle(self, *args, **options):
|
|
44
|
+
csv_path = options['csv_path']
|
|
45
|
+
language = options['language']
|
|
46
|
+
delimiter = options['delimiter'] or '|'
|
|
47
|
+
encoding = options['encoding']
|
|
48
|
+
dry_run = options['dry_run']
|
|
49
|
+
|
|
50
|
+
if not os.path.exists(csv_path):
|
|
51
|
+
raise CommandError(f'Datei nicht gefunden: {csv_path}')
|
|
52
|
+
|
|
53
|
+
models = TaxonomyModelRouter('taxonomy.sources.custom')
|
|
54
|
+
self.stdout.write(self.style.NOTICE('Quelle: taxonomy.sources.custom'))
|
|
55
|
+
self.stdout.write(self.style.NOTICE(f'Trennzeichen: {repr(delimiter)}'))
|
|
56
|
+
|
|
57
|
+
# track last Latin parent per depth to build a Latin-only hierarchy
|
|
58
|
+
self.depth_taxon = {}
|
|
59
|
+
|
|
60
|
+
with open(csv_path, 'r', encoding=encoding, newline='') as f:
|
|
61
|
+
reader = csv.reader(f, delimiter=delimiter)
|
|
62
|
+
created_count = 0
|
|
63
|
+
existing_count = 0
|
|
64
|
+
skipped_count = 0
|
|
65
|
+
skip_reasons = {}
|
|
66
|
+
skipped_examples = []
|
|
67
|
+
row_index = 0
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
with transaction.atomic():
|
|
71
|
+
for parts in reader:
|
|
72
|
+
row_index += 1
|
|
73
|
+
if not parts:
|
|
74
|
+
# Treat truly empty lines as skipped for reporting
|
|
75
|
+
skipped_count += 1
|
|
76
|
+
skip_reasons['empty line'] = skip_reasons.get('empty line', 0) + 1
|
|
77
|
+
if len(skipped_examples) < (options.get('report_limit') or 10):
|
|
78
|
+
skipped_examples.append({'row': row_index, 'reason': 'empty line', 'raw': []})
|
|
79
|
+
continue
|
|
80
|
+
c, e, s, reason = self._import_grok_row(models, parts, language)
|
|
81
|
+
created_count += c
|
|
82
|
+
existing_count += e
|
|
83
|
+
if s:
|
|
84
|
+
skipped_count += s
|
|
85
|
+
if reason:
|
|
86
|
+
skip_reasons[reason] = skip_reasons.get(reason, 0) + 1
|
|
87
|
+
if len(skipped_examples) < (options.get('report_limit') or 10):
|
|
88
|
+
skipped_examples.append({'row': row_index, 'reason': reason, 'raw': parts})
|
|
89
|
+
|
|
90
|
+
if dry_run:
|
|
91
|
+
# Print a detailed dry-run report before rolling back
|
|
92
|
+
self.stdout.write(self.style.WARNING('Dry-Run Bericht:'))
|
|
93
|
+
self.stdout.write(f' Zeilen gelesen: {row_index}')
|
|
94
|
+
self.stdout.write(f' Würde erstellen: {created_count}')
|
|
95
|
+
self.stdout.write(f' Bereits vorhanden: {existing_count}')
|
|
96
|
+
self.stdout.write(f' Übersprungen: {skipped_count}')
|
|
97
|
+
if skip_reasons:
|
|
98
|
+
self.stdout.write(' Gründe für Überspringen:')
|
|
99
|
+
for r, cnt in sorted(skip_reasons.items(), key=lambda x: (-x[1], x[0])):
|
|
100
|
+
self.stdout.write(f' - {r}: {cnt}')
|
|
101
|
+
if skipped_examples:
|
|
102
|
+
self.stdout.write(' Beispiele übersprungener Einträge:')
|
|
103
|
+
for ex in skipped_examples:
|
|
104
|
+
preview = '|'.join([str(p) for p in ex.get('raw', [])])
|
|
105
|
+
self.stdout.write(f" - Zeile {ex['row']}: {ex['reason']} -> {preview}")
|
|
106
|
+
# Trigger rollback
|
|
107
|
+
raise CommandError('Dry-Run abgeschlossen: Änderungen werden zurückgerollt.')
|
|
108
|
+
|
|
109
|
+
except CommandError as e:
|
|
110
|
+
if 'Dry-Run abgeschlossen' in str(e):
|
|
111
|
+
self.stdout.write(self.style.WARNING(str(e)))
|
|
112
|
+
return
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
self.stdout.write(self.style.SUCCESS(
|
|
116
|
+
f'Fertig. Erstellt: {created_count}, vorhanden: {existing_count}, Zeilen: {row_index}'))
|
|
117
|
+
|
|
118
|
+
def _get_or_create(self, models, parent, latname, author, rank):
|
|
119
|
+
existing_qs = models.TaxonTreeModel.objects.filter(taxon_latname=latname, rank=rank)
|
|
120
|
+
if parent is None:
|
|
121
|
+
existing_qs = existing_qs.filter(is_root_taxon=True)
|
|
122
|
+
else:
|
|
123
|
+
existing_qs = existing_qs.filter(parent=parent)
|
|
124
|
+
|
|
125
|
+
instance = existing_qs.first()
|
|
126
|
+
created = False
|
|
127
|
+
if not instance:
|
|
128
|
+
extra = {'rank': rank}
|
|
129
|
+
if parent is None:
|
|
130
|
+
extra['is_root_taxon'] = True
|
|
131
|
+
else:
|
|
132
|
+
extra['parent'] = parent
|
|
133
|
+
instance = models.TaxonTreeModel.objects.create(latname, None, **extra)
|
|
134
|
+
created = True
|
|
135
|
+
return instance, created
|
|
136
|
+
|
|
137
|
+
def _clear_deeper_depth(self, depth: int):
|
|
138
|
+
# drop any cached parents deeper than current depth
|
|
139
|
+
for d in list(self.depth_taxon.keys()):
|
|
140
|
+
if d > depth:
|
|
141
|
+
del self.depth_taxon[d]
|
|
142
|
+
|
|
143
|
+
def _import_grok_row(self, models, parts: List[str], language: Optional[str]):
|
|
144
|
+
# Normalize tokens but keep positional layout
|
|
145
|
+
parts = [(p or '').strip() for p in parts]
|
|
146
|
+
if not parts or len(parts) < 3:
|
|
147
|
+
return 0, 0, 1, 'empty or too few columns'
|
|
148
|
+
|
|
149
|
+
# Fixed GROK layout: [...path..., latin, code, rank]
|
|
150
|
+
rank = parts[-1]
|
|
151
|
+
if not rank:
|
|
152
|
+
return 0, 0, 1, 'missing rank'
|
|
153
|
+
latin = parts[-3] if len(parts) >= 3 else ''
|
|
154
|
+
if not latin:
|
|
155
|
+
return 0, 0, 1, 'missing latin name'
|
|
156
|
+
|
|
157
|
+
# Path segments are all tokens before latin
|
|
158
|
+
path_segments = [p for p in parts[:-3] if p]
|
|
159
|
+
depth = len(path_segments)
|
|
160
|
+
parent = self.depth_taxon.get(depth - 1) if depth > 0 else None
|
|
161
|
+
|
|
162
|
+
taxon, created = self._get_or_create(models, parent, latin, author=None, rank=rank)
|
|
163
|
+
|
|
164
|
+
# Store this taxon as the current parent at its depth and clear deeper cache
|
|
165
|
+
self.depth_taxon[depth] = taxon
|
|
166
|
+
self._clear_deeper_depth(depth)
|
|
167
|
+
|
|
168
|
+
# Locale: letzter Pfadteil als Bezeichnung (attach vernacular to Latin taxon)
|
|
169
|
+
if path_segments and language:
|
|
170
|
+
vernacular = path_segments[-1]
|
|
171
|
+
exists = models.TaxonLocaleModel.objects.filter(
|
|
172
|
+
taxon=taxon, name=vernacular, language=language
|
|
173
|
+
).exists()
|
|
174
|
+
if not exists:
|
|
175
|
+
models.TaxonLocaleModel.objects.create(taxon, vernacular, language, preferred=True)
|
|
176
|
+
|
|
177
|
+
return (1, 0, 0, None) if created else (0, 1, 0, None)
|
|
@@ -1797,6 +1797,10 @@ app_kit/taxonomy/sources/custom/models.py,sha256=4ExGyy2l_mYnGlsBhLndS7lQL5QSFGb
|
|
|
1797
1797
|
app_kit/taxonomy/sources/custom/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
|
1798
1798
|
app_kit/taxonomy/sources/custom/urls.py,sha256=0P71XKmzS7mLeYIMcn0EE6Vi0pRV1fu-l_d317Kpnug,1032
|
|
1799
1799
|
app_kit/taxonomy/sources/custom/views.py,sha256=ZFkqba9mGgKdbaZx4GNGI0hKoviyaEOnjf4CGvTTr8E,9780
|
|
1800
|
+
app_kit/taxonomy/sources/custom/management/commands/import_custom_species_csv.py,sha256=Ng3-FKfJpxXijxb8EzTEKXgjM8ivJiM92FAIG4ngrug,7434
|
|
1801
|
+
app_kit/taxonomy/sources/custom/management/commands/import_custom_taxonomy_csv.py,sha256=Y3QHClrZdvz5Fz-UgyTYhxtcCc6UxBa4BXpyL47PN54,8202
|
|
1802
|
+
app_kit/taxonomy/sources/custom/management/commands/__pycache__/import_custom_species_csv.cpython-313.pyc,sha256=AxUh7xot7wo__NBggEI9PEqH9lDX0GHBQ9ZkmTwLjNs,9725
|
|
1803
|
+
app_kit/taxonomy/sources/custom/management/commands/__pycache__/import_custom_taxonomy_csv.cpython-313.pyc,sha256=nU0sag8bvHUMSb6KBbs7EF8sZbdp5zwDdSiERW_PAQg,10567
|
|
1800
1804
|
app_kit/taxonomy/sources/custom/migrations/0001_initial.py,sha256=FLCqXqGQcG0-cMeyxgSt-BuE-5uNq7HfCvYXBDni4wc,4246
|
|
1801
1805
|
app_kit/taxonomy/sources/custom/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1802
1806
|
app_kit/taxonomy/sources/custom/templates/custom_taxonomy/manage_custom_taxon.html,sha256=SSXwvvZflxkKAK1GrkZM15ZD6JUfjEWDmiI78TGhmKg,1348
|
|
@@ -1935,8 +1939,8 @@ app_kit/tests/__pycache__/test_models.cpython-313.pyc,sha256=Lmv3BfjLs5Fg-olJeMl
|
|
|
1935
1939
|
app_kit/tests/__pycache__/test_utils.cpython-313.pyc,sha256=GX3REqZygi2eO_A2F2_KtYi7hg54X5QPtCTCGWuxGpM,14054
|
|
1936
1940
|
app_kit/tests/__pycache__/test_views.cpython-311.pyc,sha256=NDJR40TcMm-bXXC-wV7OgH1sGR3N7psSWYiUirkkrjU,133242
|
|
1937
1941
|
app_kit/tests/__pycache__/test_views.cpython-313.pyc,sha256=q851UqIZFCCTfQb1lF4SVxV1j_Vu1hJdOlpckmrGX28,125363
|
|
1938
|
-
localcosmos_app_kit-0.9.
|
|
1939
|
-
localcosmos_app_kit-0.9.
|
|
1940
|
-
localcosmos_app_kit-0.9.
|
|
1941
|
-
localcosmos_app_kit-0.9.
|
|
1942
|
-
localcosmos_app_kit-0.9.
|
|
1942
|
+
localcosmos_app_kit-0.9.18.dist-info/licenses/LICENCE,sha256=VnxALPSxXoU59rlNeRdJtwS_nU79IFpVWsZZCQUM4Mw,1086
|
|
1943
|
+
localcosmos_app_kit-0.9.18.dist-info/METADATA,sha256=wC25Ke7ekb_Rf0l-tcIN2Ih3__GSe64Z0SNNQwGQVSg,1388
|
|
1944
|
+
localcosmos_app_kit-0.9.18.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
1945
|
+
localcosmos_app_kit-0.9.18.dist-info/top_level.txt,sha256=F6H4pEBkCvUR_iwQHIy4K1iby-jzfWg3CTym5XJKeys,8
|
|
1946
|
+
localcosmos_app_kit-0.9.18.dist-info/RECORD,,
|
|
File without changes
|
{localcosmos_app_kit-0.9.17.dist-info → localcosmos_app_kit-0.9.18.dist-info}/licenses/LICENCE
RENAMED
|
File without changes
|
|
File without changes
|