From bf7d38a8d967c97d3af8b6adc8e7f38cdc8904fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 15 Oct 2023 17:11:59 +0200 Subject: [PATCH 01/75] La til forslag til algoritme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lagde undersøkende kode for matchingalgoritme #27 --- algorithm/CSP_test.py | 86 ++++++++++++++++++ .../__pycache__/CSP_test.cpython-311.pyc | Bin 0 -> 6170 bytes algorithm/__pycache__/CSP_test.cpython-39.pyc | Bin 0 -> 3489 bytes algorithm/testing.py | 74 +++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 algorithm/CSP_test.py create mode 100644 algorithm/__pycache__/CSP_test.cpython-311.pyc create mode 100644 algorithm/__pycache__/CSP_test.cpython-39.pyc create mode 100644 algorithm/testing.py diff --git a/algorithm/CSP_test.py b/algorithm/CSP_test.py new file mode 100644 index 00000000..a378a849 --- /dev/null +++ b/algorithm/CSP_test.py @@ -0,0 +1,86 @@ +class CSP: + def __init__(self, variables, Domains,constraints): + self.variables = variables + self.domains = Domains + self.constraints = constraints + self.solution = None + + def solve(self): + print("Test") + assignment = {} + self.solution = self.backtrack(assignment) + return self.solution + + def backtrack(self, assignment): + print(f"{len(assignment)=}\r", end="") + if len(assignment) == len(self.variables): + return assignment + + var = self.select_unassigned_variable(assignment) + for value in self.order_domain_values(var, assignment): + if self.is_consistent(var, value, assignment): + assignment[var] = value + result = self.backtrack(assignment) + if result is not None: + return result + del assignment[var] + return None + + def select_unassigned_variable(self, assignment): + unassigned_vars = [var for var in self.variables if var not in assignment] + return min(unassigned_vars, key=lambda var: len(self.domains[var])) + + def order_domain_values(self, var, assignment): + return self.domains[var] + + def is_consistent(self, var, value, assignment): + for constraint_var in self.constraints[var]: + if constraint_var in assignment and assignment[constraint_var] == value: + return False + return True + + + + +personer_og_tidsslots = {"Jørgen": {1, 2, 3, 4}, "Sindre": {2, 4, 5, 6}, "Julian": {3, 1, 6}, "Fritz": {1, 4}} +komiteer_og_tidsslots = {"Appkom": {1, 2, 3, 4, 5}, "OIL": {4, 5, 6}, "Prokom": {1, 2, 3, 5}} + +timeslots = range(1, 9) +personer = personer_og_tidsslots.keys() +komiteer = komiteer_og_tidsslots.keys() + +komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, "Sindre": {"Appkom", "OIL"}, "Julian": {"Appkom", "Prokom", "OIL"}, "Fritz": {"OIL"}} + +# Variables +variables = set() +for person in komiteer_per_person: + for komite in komiteer_per_person[person]: + variables.add((person, komite)) +# Domains +domains = {var: personer_og_tidsslots[var[0]].intersection(komiteer_og_tidsslots[var[1]]) for var in variables} + +import itertools +constraints = {var: set() for var in variables} +for var, annen in itertools.permutations(variables, 2): + if annen[0] == var[0]: + constraints[var].add(annen) + if annen[1] == var[1]: + constraints[var].add(annen) + +# # Solution +csp = CSP(variables, domains, constraints) +sol = csp.solve() +print(sol) + + +solution = {komite: [None for _ in timeslots] for komite in komiteer} +for person, komite in sol: + solution[komite][sol[person, komite]] = person + +print("---------") +print(solution) +# print_sudoku(solution) + +for komite in solution: + print(komite.ljust(15), end="|") + print("|".join([str(slot).rjust(15) for slot in solution[komite]])) diff --git a/algorithm/__pycache__/CSP_test.cpython-311.pyc b/algorithm/__pycache__/CSP_test.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18edb462bc3f2af23e8ce03ec94a63d69522f99b GIT binary patch literal 6170 zcmbstTWk|o_Rbx9{Ei(vF-~|TBq10`ps+<0Xei{-E@V?^pbsn5xSjz_96OzHP-~gh z1VzHEvRcz^l}Ou4mfB@gwX5Z0KalpP(*EpMHIAmj8mSV}?oa-*Vk;rO_MAI@L#wX# z`p%jAJolV)&ujcAkH<-%{OyN*(tT+C6Fb#nuQVQi%M-FfD0!8LWQmB}Jxa(|@R%h+ zt`U)b&H>~Zw0zEqR#CV{Mcb#A2#(v?xPw6_%(_11Ba{+}hjs22Y!-?7`fCRaY=T`)m1wW#=0VI z$yxN&h8h}@OLAYK_c)*s9+Mxs zl2hz#mJ`quoq;=u_#*)+Ao}Z+O8&;)EW0OptK^d!*8B(;fmRLxv$Y9xL+}5?ynGor z1j$HklJ6eq*H`eECDI{v6g#WE#&?ShgMeX+6l|qg41z>$&qzEZ1wm#ZCNme@q)Q4( z?grk1<{j2YPQcO2U3iALrS%MUa;SAw3c(5IB5pS8F4j>sFd??=61%?cZoXL#EDg@= zDt0$)t2M=~z+DsNt(-Pf)!YM}JTC^ONqAelh7lT7oqc==IxBE55l$o#DsoFC!iyB3 zMdSfmMGHVdv;wq=0zkWH18jDKADuoIw>02HwZkiYJi<`sxEzVrrgIc})k8^+hhEK{ zhy;2yWe~|U`I4IoTMWmIn4E~cmy{I4CZ^|NiIifv;^~xy^J5!Lv9w%ijHTlXa}wa3 zNv3CG$?4g2Qks|3SLN8;oFpewS7&1L^Rwx>nb+s%GqG7k`B2G7b2A`p!}A{+ z_GmPbN@SwZtQ&@ht3BjW=>wpWa$uX1Vvie{E?syr0_DG}j=bX1Q0KBF%BrpvT1qDyq~F(n`-wu!drz?bMcn zowNj7a4;usg1tOH^uc-_cEG81cPjVcSdOAQgMBEqXUa1aS#p*|c;b`;`@`KYljWDu z7Q@!5tS>30_SA1X{PI)IV}O+!yp$5dHW{Oq!NVfc0K35!rFbT~kTUm3Vzg??Mj$PV zk{mVd8HSS!VBVgD5=FyKfK{b3vLDcetxU<}07DJi4J%u02(qLsBr^(T3~1M;(y={R z52{>ESy6wKZvim1)9gEcdpMuHb8`74G|gIkL!*02bdOH=K;wI>J>R_;U5b9jua12> zRSNCXL;KdP`!)&3cb1(U8sG8A?tW+=T(=LdIZO7#y8W=)SM~-TdI#6NgKPX+q~txS zdyj4sfe$gDN@cO|S*fitavvfHm-!e80$FC5N$pMYuPJD#-*6a}-33(&YtZgVRn&t2ibncMG> z>o8`P*06DtHfcD`6WBwS>|gWERsuR;nQC%NsJeJDkGBE%1dh83igw8k?uWZWZ*YGR za@<|I>;OkjyB}aj+@tjN>D8* zBc{UJps@nlR9D*^R~LL$)SM~h3Tv%;RbdBFWw#(Z&+$3EFsXO}02dH&wiV*Kqg$ih zCTXY@)q-R((k7wt8I8`R#f2oc z-O=d#3$dg*Bjcx%+<^dnxQq%V<2A@gDRTu(zRd1r6e+`zF79AgtQrpfXz za12d-1FdU>(fi0qf$ruw?-qK_$;Lgy}>|lqEwrvye>0QigRxPGqtph|Ju> z4q$k2xBMai^MG2H9bi(!PO0y}#o%IvQ!Ga{_}70qX$a@!G$gDWn6Sym5S&189Ds=z zTlI-1_m24*GWYn`09Hr>o%=C#?iuJ;OAbC5>Ka~eJwN}*{XJbi zj@jr2?qV_m7CYx5c~7Sxfk0}zHEd|w}I zKo_q_8F>`ijeIcL8Nd@2WD^)@J7Ct;cwYHr^zQh_wh)mY&|t-}}uaXB)HIf*F>#KnHtU>2PFqmK~IJ3{}ep#+9dBPDbjQ#8DIO8!%HzoBD5ZjX6haX8wG85=pbQ zfH((G_%c!-rl2Aj$e88p3-BFRVcNn(z><+lfJEWS2;PK8*$V(f={s%KwT@2$g$sAP zKJHrC`O(gzztrBZxA)hkP2p}eo%k!TRXz;>j*O+QO7ape|I{bm=E%Sc#mVC21NPG{T&mQ>buBrkT}x@9G*;4C?mJXGSv>jRCH!gUW-2vw zNrOr{AGzD}VBIxPat-RPLDl}uz@xr>Yj112j(l_U9~ZUL z(^@oZ)=ffQrIQq16ZBX3+A>vv8VPI)#N*44-WgvWhcnuGA%=306aav)5bv(y5#75N z!zhdaP~R+5*X^NOLxtdKQ1kVaXs=FtHC91Puu34Vc@7<*lmb|CrgdjpwLKy}ty;^> zBNg8PH&kjnptl`RU1k5CLRk0jS?jugs^mYS`;S1V=5{c!ct#z~ca|-VeDBh01-@|V zV`1GAL~3@FsV5(YKTsah;5rQ!7D{xxPT`M(?f={ACqT5#smAl+!dt7eC1I~F>{a=) z;JBT*l~|lznuQLrs#Z0==v;EDPFU`q;K24Dyau4;8rNOpa2>%eAc})8I{=GQ>O_8` z96q3i^1F4ez0g;`evSCe1FkK4m%OSM4)8+iL%DHG zX#m1iaDv*Y-ngB+l`EVs*|zAmEvwTS*IVLxb*>j0<^tvR2e&>jE%biq!EXJ9Q_v~- aPU`?|qq=Rh#Et3Pn8xa+wSEFK=Klgh*(%fk literal 0 HcmV?d00001 diff --git a/algorithm/__pycache__/CSP_test.cpython-39.pyc b/algorithm/__pycache__/CSP_test.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca0ff869327b0aeb570a51c80e23ea4b9af8bd8f GIT binary patch literal 3489 zcma)8OK%*-7Ov{<8TULr89&Df!Q9JhAXovhii`z{1tLNL$*^e|_3cbmoQ`|C$5lO+ zVvX5MB(gyQv1Kt9tNa7@{0A1jWi<;{hz(-FcdFg?*a_&)s7_VaId$IWR0WMjmErgH zZ*R&&tbV1B+20&K-p9~CamGGkf(iZ@V{Ccs0Tb4b9MCq<`H_o~C_k{o+z*aN`>xrq z7+3|i_Ji$N(gsx>=N-u>r4#0rrDf(h(GX3XZix8@7EZTtV*UX)++0~1Gp{P$ZT985 zSCg)+br{xduP&>ThOEi@ed~mCcJS@NV{t{aA2V6qW@GL(#e!Ij=Dm5bG_%{1^OGyG zDwa7sNt`$u_}#OZ_EVvfW|ezCfITEg38zu)kcy9{eI9Q8s03Yl@0Byl`*i(MD>tvO~K!w4*Na}1(B>f`QRmjgtK!+SyMcIZ(--3i^Gi!gy~t7!3B4{_dxPK^p99{Ya;>zY7!nRO)nN@F;VA zKa9iF_sK?05*aU>|4|)u8wI+Dk~#YkEop3gvW!K@j^ULFd~-tH!Fb4bm?FBal~wKq zy?xl;+gD|?qo@+IYM^zv7n1?T)Era@Rf2;KC26DS1mg~`^P>tWWH3GAT>QveL}j#z zut=fDm;*wKF-L+gkDv|ovq-;VL~ddue%utytz*P+;^5oHj;(IcJhC>4zGUc~kt5iU ztN)E0i*ZDm@b9sM8x--}#&TvyGB#1n)(}wHOZ{P-+b4xz&(XStYnC@vXXijUG|qb(*UieU@+e1j#cF?-sCQ?V$lDn zLEbxy#03vhEXj*B>(>FkX7{-%AomG>h~_kg*HT#hHOD>`lq*W#SSXnC-DeVm3TA;A ztpe=9jbFBq9Q+A83#6L8P~^C=Opxk}{brHA*(INZAu)%7}GfXAx0L5k`?QQ0r7XO4}Lj z+|4NH-xI;s3$#z>9^L%^b<2--d1mj+M_nsV9=jjLUHd%v-u%Th(ndU}DKm01Di}2n za1}$F*2Ne3(e;1Vq(>eoezc~Fx{aaf4n^f4%R6WJjmTlQP>j4W6Dbo)qeTmd7csOc zPkyvQIy@)kFErR9@r>{doGygZ&2o(58`+0URZ*5s2W=PST*WPfKpj*CrJ#NH!_N^S zVpEijQINuGwDg+iy_ZTckKno1S3vk5jMMJdg!fP`S4j5@m3g;Yp>`Pued+sI)%W{} z7)G?N`~JZ&h;ob~2P&#YQ{ghTy z^6QkMHKjd+Q0rJJ(tk=Wm{KR)A`(TBP%ja9`RtSEy3UXi`SdRF$~$2!l=RAXhEW*A zUg<*>rbk=EEN8J!E5nCQtN#Mz0?s~-fLt3R^!x-7ckQhFfw_k=`_qp<$;zLp1eem( zq|7VC@-+gl0_1$n9(@|bh1#{SOMbhJpEiA&PM&se3PY|TE?2BG?qBkVTEX@mA~St) z>D)gvZ5j4e(y%X$^U>4{t8(u3%lych=lm*f6~8I9zCZbhd?jP?j3H-ESOc9gqC=uLM4Y3ysQuz_&G)%0Q%})HTA0Apxqk)U=zf9 zQr)DrgEo(+Uy*p3TVz+ZP^^3)X&^Hdb;A5kD4*`UO Date: Sun, 18 Feb 2024 14:09:30 +0100 Subject: [PATCH 02/75] snapshot: add snapshot Snapshot for tracking previously local files. #27 --- .gitignore | 5 +- .../CSP_test.py | 0 .../testing.py | 0 .../Applicant.py | 41 ++++ .../Committee.py | 28 +++ .../Modellering.md | 52 +++++ .../fixed_test.py | 177 ++++++++++++++++++ .../mip_test.py | 70 +++++++ algorithm/README.md | 3 + .../__pycache__/CSP_test.cpython-311.pyc | Bin 6170 -> 0 bytes algorithm/__pycache__/CSP_test.cpython-39.pyc | Bin 3489 -> 0 bytes 11 files changed, 375 insertions(+), 1 deletion(-) rename algorithm/{ => Constraint Satisfaction Problem}/CSP_test.py (100%) rename algorithm/{ => Constraint Satisfaction Problem}/testing.py (100%) create mode 100644 algorithm/Mixed Integer Linear Programming/Applicant.py create mode 100644 algorithm/Mixed Integer Linear Programming/Committee.py create mode 100644 algorithm/Mixed Integer Linear Programming/Modellering.md create mode 100644 algorithm/Mixed Integer Linear Programming/fixed_test.py create mode 100644 algorithm/Mixed Integer Linear Programming/mip_test.py create mode 100644 algorithm/README.md delete mode 100644 algorithm/__pycache__/CSP_test.cpython-311.pyc delete mode 100644 algorithm/__pycache__/CSP_test.cpython-39.pyc diff --git a/.gitignore b/.gitignore index 2b7ceea6..bbaa3429 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ yarn-error.log* next-env.d.ts # SWC -.babelrc \ No newline at end of file +.babelrc + +# Python +__pycache__/ \ No newline at end of file diff --git a/algorithm/CSP_test.py b/algorithm/Constraint Satisfaction Problem/CSP_test.py similarity index 100% rename from algorithm/CSP_test.py rename to algorithm/Constraint Satisfaction Problem/CSP_test.py diff --git a/algorithm/testing.py b/algorithm/Constraint Satisfaction Problem/testing.py similarity index 100% rename from algorithm/testing.py rename to algorithm/Constraint Satisfaction Problem/testing.py diff --git a/algorithm/Mixed Integer Linear Programming/Applicant.py b/algorithm/Mixed Integer Linear Programming/Applicant.py new file mode 100644 index 00000000..eda06bdc --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/Applicant.py @@ -0,0 +1,41 @@ +from Committee import Committee +from fixed_test import TimeInterval + +import itertools + +class Applicant: + """ + Klasse som holder styr over en søker, med data om hvilke + komitéer hen har søkt på, og når søkeren kan ha intervjuer.""" + def __init__(self): + self.committee: list[Committee] = [] + self.slots: set[TimeInterval] = [] + + def add_committee(self, committee: Committee) -> None: + self.committee.append(committee) + + def add_committees(self, committees: set[Committee]) -> None: + for committee in committees: + self.add_committee(committee) + + def add_interval(self, interval: TimeInterval) -> None: + # TODO: Vurder å gjøre "sanitizing" ved å slå sammen overlappende intervaller. + self.slots.add(interval) + + def add_intervals(self, intervals: set[TimeInterval]) -> None: + for interval in intervals: + self.add_interval(interval) + + def get_fitting_committee_slots(self, committee: Committee) -> set[TimeInterval]: + """ + Returnerer alle tidsintervallene i *komiteen* + som er inneholdt i et av *self* sine intervaller. + """ + + result: set[TimeInterval] = set() + + for applicant_interval, committee_interval in itertools.product(self.slots, committee.slots): + if applicant_interval.contains(committee_interval): + result.add(committee_interval) + + return result diff --git a/algorithm/Mixed Integer Linear Programming/Committee.py b/algorithm/Mixed Integer Linear Programming/Committee.py new file mode 100644 index 00000000..ef338954 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/Committee.py @@ -0,0 +1,28 @@ +from fixed_test import TimeInterval + + +class Committee: + """ + En klasse som representerer en komité + og holder oversikt over når komitéene kan ha + møte og hvor lange intervjuene er. + """ + + def __init__(self, interview_length: int = 1): + self.capacities: dict[TimeInterval, int] = dict() + self.interview_length: int = interview_length + + def add_interval(self, interval: TimeInterval, capacity: int = 1) -> None: + """Legger til et nytt intervall med gitt kapasitet hvis intervallet + ikke allerede har en kapasitet for denne komitéen. + Når intervaller legges til deles det automatisk opp i + intervaller med lik lengde som intervjulengder.""" + minimal_intervals = TimeInterval.divide_interval(interval=interval, length=self.interview_length) + for interval in minimal_intervals: + if interval not in self.capacities: + self.capacities[interval] = capacity + + def add_intervals_with_capacities(self, intervals_with_capacities: dict[TimeInterval, int]): + """Legger til flere tidsintervaller samtidig.""" + for interval, capacity in intervals_with_capacities.items(): + self.add_interval(interval, capacity) diff --git a/algorithm/Mixed Integer Linear Programming/Modellering.md b/algorithm/Mixed Integer Linear Programming/Modellering.md new file mode 100644 index 00000000..3fea01e0 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/Modellering.md @@ -0,0 +1,52 @@ +# Modellering av problem gjennom Mixed Integer Linear Programming + +## Nyttige ressurser + +- https://python-mip.readthedocs.io/en/latest/quickstart.html +- https://towardsdatascience.com/mixed-integer-linear-programming-1-bc0ef201ee87 +- https://towardsdatascience.com/mixed-integer-linear-programming-formal-definition-and-solution-space-6b3286d54892 +- https://www.gurobi.com/resources/mixed-integer-programming-mip-a-primer-on-the-basics/ + +## Variabler + +`p` +- Person + +`k` +- Komité + +`t` +- Timeslot (Må gjøres til intervaller etter hvert) + +`m(p, k, t)` +- Binær variabel +- Person `p` har møte med komité `k` i timeslot `t` + +## Hjelpevariabler + +`c(p, t)` +- Binære variabler +- Tidspunkt `t` passer for person `p` + +`c(k, t)` +- Heltallsvariabel +- Kapasitet for komité `k` på tidspunkt `t` (hvor mange intervju de kan ha på det gitte tidspunktet) + +## Begrensninger + +For alle `p`: + +- `m(p, k, t) <= 1` dersom + - `p` har søkt på komité `k` + - `c(p, t) => 1` + - `c(k, t) => 1` +- `m(p, k, t) <= 0` ellers + +For alle `k`: +- `sum(m(p, k, t)) <= c(k, t)` for alle personer `p` og tidspunkt `t` + + + +## Mål + +Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t` diff --git a/algorithm/Mixed Integer Linear Programming/fixed_test.py b/algorithm/Mixed Integer Linear Programming/fixed_test.py new file mode 100644 index 00000000..5d7ca480 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/fixed_test.py @@ -0,0 +1,177 @@ +""" +TODO: +- [Gjort] Lage funksjon som deler opp fra en komités slot +- Sette opp begrensningene fra modelleringen +- Flikke litt på modelleringen. +- Finn ut hvordan man kan preprosessere dataen for å få ned kjøretiden (f. eks ved å lage lister av personer for hver komité.) +""" + +from __future__ import annotations +from dataclasses import dataclass + +import itertools + +from Committee import Committee +from Applicant import Applicant + + +@dataclass(frozen=True) +class TimeInterval: + start: int + end: int + + def overlaps(self, other: TimeInterval) -> bool: + """Returnerer true om to timeslots er helt eller delvis overlappende.""" + return other.start < self.start < other.end or self.start < other.start < self.end + + def contains(self, other: TimeInterval) -> bool: + """Returnerer true om other inngår helt i self.""" + return self.start <= other.start and other.end <= self.end + + def intersection(self, other: TimeInterval) -> TimeInterval: + """Returnerer et snitt av to timeslots.""" + if not self.overlaps(other): + # Snittet er tomt grunnet ingen overlapp + return None + + start = max(self.start, other.start) + end = min(self.end, other.end) + return TimeInterval(start, end) + + def get_contained_slots(self, slots: list[TimeInterval]): + """Returnerer en delmengde av de slots i listen av timeslots + "slots", som inngår helt i dette timeslottet.""" + return set(slot for slot in slots if self.contains(slot)) + + @staticmethod + def divide_interval(interval: TimeInterval, length: int) -> list[TimeInterval]: + """ + + Deler opp et intervall i mindre intervaller av lengde *length*. + + Note: + - Det antas at intervallet kan deles opp i hele deler av lengde *length*. + Overskytende tid vil bli ignorert. + """ + result = [] + global_start = interval.start + local_start = global_start + local_end = local_start + length + while local_end < interval.end: + result.append(TimeInterval(local_start, local_end)) + + return result + +""" +Dette er gammel kode som nå er flyttet til de passende komité-/søker-klassene. +Foreløpig beholdt for referanse. +""" +# class TimeIntervals: +# def __init__(self, initial_list: list[TimeInterval] = None): +# self.list: list[TimeInterval] = initial_list if initial_list else [] + +# def add(self, interval: TimeInterval): +# self.list.append(interval) + +# def recursive_intersection(self, other: TimeIntervals): +# """ +# Returnerer alle tidsintervallene i *other* som er inneholdt i et av *self* sine intervaller""" +# result = TimeIntervals() + +# for self_interval, other_interval in itertools.product(self.list, other.list): +# if self_interval.contains(other_interval): +# result.add(other_interval) + +# return result + +# def __iter__(self): +# return self.list.__iter__() + + +committees: set[Committee] + +appkom = Committee() +appkom.add_intervals_with_capacities({TimeInterval(1, 2): 1, + TimeInterval(2, 3): 1, + TimeInterval(3, 4): 1, + TimeInterval(4, 5): 1}) + +oil = Committee() +oil.add_intervals_with_capacities({TimeInterval(4, 5): 1, + TimeInterval(5, 6): 1}) + +prokom = Committee() +prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, + TimeInterval(4, 6): 1}) + +committees = {appkom, oil, prokom} + +jørgen: Applicant = Applicant() +jørgen.add_committees({appkom, prokom}) +jørgen.add_intervals({TimeInterval(1, 4)}) + +sindre: Applicant = Applicant() +sindre.add_committees({appkom, oil}) +sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) + +julian: Applicant = Applicant() +julian.add_committees({appkom, prokom, oil}) +julian.add_intervals({TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) + +fritz: Applicant = Applicant() +fritz.add_committees({oil}) +fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) + +applicants: set[Applicant] = {jørgen, sindre, julian, fritz} + + +# personer_og_tidsslots = {"Jørgen": {TimeInterval(1, 4)}, +# "Sindre": {TimeInterval(2, 3), TimeInterval(4, 6)}, +# "Julian": {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}, +# "Fritz": {TimeInterval(1, 2), TimeInterval(4, 5)}} +# komiteer_og_tidsslots = {"Appkom": {TimeInterval(1, 2), TimeInterval(2, 3), TimeInterval(3, 4), TimeInterval(4, 5)}, +# "OIL": {TimeInterval(4, 5), TimeInterval(5, 6)}, +# "Prokom": {TimeInterval(1, 3), TimeInterval(4, 6)}} + +# komite_kapasiteter = {"Appkom": {TimeInterval(1, 2): 1, +# TimeInterval(2, 3): 1, +# TimeInterval(3, 4): 1, +# TimeInterval(4, 5): 1}, +# "OIL": {TimeInterval(4, 5): 1, +# TimeInterval(5, 6): 1}, +# "Prokom": {TimeInterval(1, 3): 1, +# TimeInterval(4, 6): 1}} + + +# personer = personer_og_tidsslots.keys() +# komiteer = komiteer_og_tidsslots.keys() + +# intervjulengder = {"Appkom": 1, +# "Prokom": 2, +# "OIL": 1} + +# komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, +# "Sindre": {"Appkom", "OIL"}, +# "Julian": {"Appkom", "Prokom", "OIL"}, +# "Fritz": {"OIL"}} + + +""" +Tror det følgende er gammel kode som ble brukt for CSP. +""" + +# # Variables +# variables = set() +# for person in komiteer_per_person: +# for komite in komiteer_per_person[person]: +# variables.add((person, komite)) +# # Domains +# domains = {var: personer_og_tidsslots[var[0]].intersection( +# komiteer_og_tidsslots[var[1]]) for var in variables} + +# constraints = {var: set() for var in variables} +# for var, annen in itertools.permutations(variables, 2): +# if annen[0] == var[0]: +# constraints[var].add(annen) +# if annen[1] == var[1]: +# constraints[var].add(annen) diff --git a/algorithm/Mixed Integer Linear Programming/mip_test.py b/algorithm/Mixed Integer Linear Programming/mip_test.py new file mode 100644 index 00000000..08ea7db9 --- /dev/null +++ b/algorithm/Mixed Integer Linear Programming/mip_test.py @@ -0,0 +1,70 @@ +import mip +import itertools + +from fixed_test import TimeIntervals + +from fixed_test import komiteer_og_tidsslots, komiteer, komite_kapasiteter +from fixed_test import komiteer_per_person, personer, personer_og_tidsslots + +model = mip.Model(sense=mip.MAXIMIZE) + +m = {} + +# Lager alle maksimeringsvariabler +for person in personer: + + for komite in komiteer_per_person[person]: + for person_intervall, komite_intervall in itertools.product(personer_og_tidsslots[person], + komiteer_og_tidsslots[komite]): + # Går gjennom alle kombinasjoner av intervaller for person og for komité + + if person_intervall.contains(komite_intervall): + # Legger til alle intervaller for komitéen som passer for personen + + m[(person, komite, komite_intervall) + ] = model.add_var(var_type=mip.BINARY, name=f"({person}, {komite}, {komite_intervall})") + +print(m) + +# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. +for komite in komiteer: + for slot in komiteer_og_tidsslots[komite]: + model += mip.xsum(m[(person, komite, slot)] + for person in personer if (person, komite, slot) in m) <= komite_kapasiteter[komite][slot] + + +# Legger inn begrensninger for at en person kun har ett intervju med hver komité +for person in personer: + for komite in komiteer: + # if person == "Jørgen": + # print(person, komite) + # print(personer_og_tidsslots[person]) + # print(komiteer_og_tidsslots[komite]) + # print(personer_og_tidsslots[person].intersection( + # komiteer_og_tidsslots[komite])) + + person_tider: TimeIntervals = TimeIntervals(personer_og_tidsslots[person]) + komite_tider: TimeIntervals = TimeIntervals(komiteer_og_tidsslots[komite]) + + model += mip.xsum(m[(person, komite, slot)] for slot in person_tider.recursive_intersection(komite_tider) if komite in komiteer_per_person[person]) <= 1 + +model.objective = mip.maximize(mip.xsum(m.values())) + +solver_status = model.optimize() + + +# Møtetider: +antall_matchede_møter: int = 0 +for name, variable in m.items(): + if variable.x: + antall_matchede_møter += 1 + print(f"{name}: {variable.x}") + +antall_ønskede_møter = sum( + len(komiteer_per_person[person]) for person in personer) + +print( + f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") + + +print(solver_status) diff --git a/algorithm/README.md b/algorithm/README.md new file mode 100644 index 00000000..12684a6b --- /dev/null +++ b/algorithm/README.md @@ -0,0 +1,3 @@ +# Algoritme + +Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering. diff --git a/algorithm/__pycache__/CSP_test.cpython-311.pyc b/algorithm/__pycache__/CSP_test.cpython-311.pyc deleted file mode 100644 index 18edb462bc3f2af23e8ce03ec94a63d69522f99b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6170 zcmbstTWk|o_Rbx9{Ei(vF-~|TBq10`ps+<0Xei{-E@V?^pbsn5xSjz_96OzHP-~gh z1VzHEvRcz^l}Ou4mfB@gwX5Z0KalpP(*EpMHIAmj8mSV}?oa-*Vk;rO_MAI@L#wX# z`p%jAJolV)&ujcAkH<-%{OyN*(tT+C6Fb#nuQVQi%M-FfD0!8LWQmB}Jxa(|@R%h+ zt`U)b&H>~Zw0zEqR#CV{Mcb#A2#(v?xPw6_%(_11Ba{+}hjs22Y!-?7`fCRaY=T`)m1wW#=0VI z$yxN&h8h}@OLAYK_c)*s9+Mxs zl2hz#mJ`quoq;=u_#*)+Ao}Z+O8&;)EW0OptK^d!*8B(;fmRLxv$Y9xL+}5?ynGor z1j$HklJ6eq*H`eECDI{v6g#WE#&?ShgMeX+6l|qg41z>$&qzEZ1wm#ZCNme@q)Q4( z?grk1<{j2YPQcO2U3iALrS%MUa;SAw3c(5IB5pS8F4j>sFd??=61%?cZoXL#EDg@= zDt0$)t2M=~z+DsNt(-Pf)!YM}JTC^ONqAelh7lT7oqc==IxBE55l$o#DsoFC!iyB3 zMdSfmMGHVdv;wq=0zkWH18jDKADuoIw>02HwZkiYJi<`sxEzVrrgIc})k8^+hhEK{ zhy;2yWe~|U`I4IoTMWmIn4E~cmy{I4CZ^|NiIifv;^~xy^J5!Lv9w%ijHTlXa}wa3 zNv3CG$?4g2Qks|3SLN8;oFpewS7&1L^Rwx>nb+s%GqG7k`B2G7b2A`p!}A{+ z_GmPbN@SwZtQ&@ht3BjW=>wpWa$uX1Vvie{E?syr0_DG}j=bX1Q0KBF%BrpvT1qDyq~F(n`-wu!drz?bMcn zowNj7a4;usg1tOH^uc-_cEG81cPjVcSdOAQgMBEqXUa1aS#p*|c;b`;`@`KYljWDu z7Q@!5tS>30_SA1X{PI)IV}O+!yp$5dHW{Oq!NVfc0K35!rFbT~kTUm3Vzg??Mj$PV zk{mVd8HSS!VBVgD5=FyKfK{b3vLDcetxU<}07DJi4J%u02(qLsBr^(T3~1M;(y={R z52{>ESy6wKZvim1)9gEcdpMuHb8`74G|gIkL!*02bdOH=K;wI>J>R_;U5b9jua12> zRSNCXL;KdP`!)&3cb1(U8sG8A?tW+=T(=LdIZO7#y8W=)SM~-TdI#6NgKPX+q~txS zdyj4sfe$gDN@cO|S*fitavvfHm-!e80$FC5N$pMYuPJD#-*6a}-33(&YtZgVRn&t2ibncMG> z>o8`P*06DtHfcD`6WBwS>|gWERsuR;nQC%NsJeJDkGBE%1dh83igw8k?uWZWZ*YGR za@<|I>;OkjyB}aj+@tjN>D8* zBc{UJps@nlR9D*^R~LL$)SM~h3Tv%;RbdBFWw#(Z&+$3EFsXO}02dH&wiV*Kqg$ih zCTXY@)q-R((k7wt8I8`R#f2oc z-O=d#3$dg*Bjcx%+<^dnxQq%V<2A@gDRTu(zRd1r6e+`zF79AgtQrpfXz za12d-1FdU>(fi0qf$ruw?-qK_$;Lgy}>|lqEwrvye>0QigRxPGqtph|Ju> z4q$k2xBMai^MG2H9bi(!PO0y}#o%IvQ!Ga{_}70qX$a@!G$gDWn6Sym5S&189Ds=z zTlI-1_m24*GWYn`09Hr>o%=C#?iuJ;OAbC5>Ka~eJwN}*{XJbi zj@jr2?qV_m7CYx5c~7Sxfk0}zHEd|w}I zKo_q_8F>`ijeIcL8Nd@2WD^)@J7Ct;cwYHr^zQh_wh)mY&|t-}}uaXB)HIf*F>#KnHtU>2PFqmK~IJ3{}ep#+9dBPDbjQ#8DIO8!%HzoBD5ZjX6haX8wG85=pbQ zfH((G_%c!-rl2Aj$e88p3-BFRVcNn(z><+lfJEWS2;PK8*$V(f={s%KwT@2$g$sAP zKJHrC`O(gzztrBZxA)hkP2p}eo%k!TRXz;>j*O+QO7ape|I{bm=E%Sc#mVC21NPG{T&mQ>buBrkT}x@9G*;4C?mJXGSv>jRCH!gUW-2vw zNrOr{AGzD}VBIxPat-RPLDl}uz@xr>Yj112j(l_U9~ZUL z(^@oZ)=ffQrIQq16ZBX3+A>vv8VPI)#N*44-WgvWhcnuGA%=306aav)5bv(y5#75N z!zhdaP~R+5*X^NOLxtdKQ1kVaXs=FtHC91Puu34Vc@7<*lmb|CrgdjpwLKy}ty;^> zBNg8PH&kjnptl`RU1k5CLRk0jS?jugs^mYS`;S1V=5{c!ct#z~ca|-VeDBh01-@|V zV`1GAL~3@FsV5(YKTsah;5rQ!7D{xxPT`M(?f={ACqT5#smAl+!dt7eC1I~F>{a=) z;JBT*l~|lznuQLrs#Z0==v;EDPFU`q;K24Dyau4;8rNOpa2>%eAc})8I{=GQ>O_8` z96q3i^1F4ez0g;`evSCe1FkK4m%OSM4)8+iL%DHG zX#m1iaDv*Y-ngB+l`EVs*|zAmEvwTS*IVLxb*>j0<^tvR2e&>jE%biq!EXJ9Q_v~- aPU`?|qq=Rh#Et3Pn8xa+wSEFK=Klgh*(%fk diff --git a/algorithm/__pycache__/CSP_test.cpython-39.pyc b/algorithm/__pycache__/CSP_test.cpython-39.pyc deleted file mode 100644 index ca0ff869327b0aeb570a51c80e23ea4b9af8bd8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3489 zcma)8OK%*-7Ov{<8TULr89&Df!Q9JhAXovhii`z{1tLNL$*^e|_3cbmoQ`|C$5lO+ zVvX5MB(gyQv1Kt9tNa7@{0A1jWi<;{hz(-FcdFg?*a_&)s7_VaId$IWR0WMjmErgH zZ*R&&tbV1B+20&K-p9~CamGGkf(iZ@V{Ccs0Tb4b9MCq<`H_o~C_k{o+z*aN`>xrq z7+3|i_Ji$N(gsx>=N-u>r4#0rrDf(h(GX3XZix8@7EZTtV*UX)++0~1Gp{P$ZT985 zSCg)+br{xduP&>ThOEi@ed~mCcJS@NV{t{aA2V6qW@GL(#e!Ij=Dm5bG_%{1^OGyG zDwa7sNt`$u_}#OZ_EVvfW|ezCfITEg38zu)kcy9{eI9Q8s03Yl@0Byl`*i(MD>tvO~K!w4*Na}1(B>f`QRmjgtK!+SyMcIZ(--3i^Gi!gy~t7!3B4{_dxPK^p99{Ya;>zY7!nRO)nN@F;VA zKa9iF_sK?05*aU>|4|)u8wI+Dk~#YkEop3gvW!K@j^ULFd~-tH!Fb4bm?FBal~wKq zy?xl;+gD|?qo@+IYM^zv7n1?T)Era@Rf2;KC26DS1mg~`^P>tWWH3GAT>QveL}j#z zut=fDm;*wKF-L+gkDv|ovq-;VL~ddue%utytz*P+;^5oHj;(IcJhC>4zGUc~kt5iU ztN)E0i*ZDm@b9sM8x--}#&TvyGB#1n)(}wHOZ{P-+b4xz&(XStYnC@vXXijUG|qb(*UieU@+e1j#cF?-sCQ?V$lDn zLEbxy#03vhEXj*B>(>FkX7{-%AomG>h~_kg*HT#hHOD>`lq*W#SSXnC-DeVm3TA;A ztpe=9jbFBq9Q+A83#6L8P~^C=Opxk}{brHA*(INZAu)%7}GfXAx0L5k`?QQ0r7XO4}Lj z+|4NH-xI;s3$#z>9^L%^b<2--d1mj+M_nsV9=jjLUHd%v-u%Th(ndU}DKm01Di}2n za1}$F*2Ne3(e;1Vq(>eoezc~Fx{aaf4n^f4%R6WJjmTlQP>j4W6Dbo)qeTmd7csOc zPkyvQIy@)kFErR9@r>{doGygZ&2o(58`+0URZ*5s2W=PST*WPfKpj*CrJ#NH!_N^S zVpEijQINuGwDg+iy_ZTckKno1S3vk5jMMJdg!fP`S4j5@m3g;Yp>`Pued+sI)%W{} z7)G?N`~JZ&h;ob~2P&#YQ{ghTy z^6QkMHKjd+Q0rJJ(tk=Wm{KR)A`(TBP%ja9`RtSEy3UXi`SdRF$~$2!l=RAXhEW*A zUg<*>rbk=EEN8J!E5nCQtN#Mz0?s~-fLt3R^!x-7ckQhFfw_k=`_qp<$;zLp1eem( zq|7VC@-+gl0_1$n9(@|bh1#{SOMbhJpEiA&PM&se3PY|TE?2BG?qBkVTEX@mA~St) z>D)gvZ5j4e(y%X$^U>4{t8(u3%lych=lm*f6~8I9zCZbhd?jP?j3H-ESOc9gqC=uLM4Y3ysQuz_&G)%0Q%})HTA0Apxqk)U=zf9 zQr)DrgEo(+Uy*p3TVz+ZP^^3)X&^Hdb;A5kD4*`UO Date: Sun, 3 Mar 2024 16:56:12 +0100 Subject: [PATCH 03/75] docs: add venv-guide --- .gitignore | 3 ++- algorithm/README.md | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index bbaa3429..da2a43d7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,5 @@ next-env.d.ts .babelrc # Python -__pycache__/ \ No newline at end of file +__pycache__/ +.venv/ \ No newline at end of file diff --git a/algorithm/README.md b/algorithm/README.md index 12684a6b..af9f9141 100644 --- a/algorithm/README.md +++ b/algorithm/README.md @@ -1,3 +1,17 @@ # Algoritme -Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering. +Prøvde først med CSP-algoritme, men har nå gått over til MIP-programmering (Mixd Integer Linear Programming). + +## Setup Python Venv + +```bash +cd algorithm +python -m venv ".venv" +``` + +Lag så en fil i `.\.venv\Lib\site-packages` som slutter på `.pth` og inneholder den absolutte filstien til `mip_matching`-mappen. + +``` +.\.venv\Scripts\activate +python -m pip install -r requirements.txt +``` From ff875572c298b977dd0db8a21205677def8ed72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 3 Mar 2024 16:57:43 +0100 Subject: [PATCH 04/75] docs: add modelling of MILP-algorithm --- algorithm/mip_matching/Modellering.md | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 algorithm/mip_matching/Modellering.md diff --git a/algorithm/mip_matching/Modellering.md b/algorithm/mip_matching/Modellering.md new file mode 100644 index 00000000..3fea01e0 --- /dev/null +++ b/algorithm/mip_matching/Modellering.md @@ -0,0 +1,52 @@ +# Modellering av problem gjennom Mixed Integer Linear Programming + +## Nyttige ressurser + +- https://python-mip.readthedocs.io/en/latest/quickstart.html +- https://towardsdatascience.com/mixed-integer-linear-programming-1-bc0ef201ee87 +- https://towardsdatascience.com/mixed-integer-linear-programming-formal-definition-and-solution-space-6b3286d54892 +- https://www.gurobi.com/resources/mixed-integer-programming-mip-a-primer-on-the-basics/ + +## Variabler + +`p` +- Person + +`k` +- Komité + +`t` +- Timeslot (Må gjøres til intervaller etter hvert) + +`m(p, k, t)` +- Binær variabel +- Person `p` har møte med komité `k` i timeslot `t` + +## Hjelpevariabler + +`c(p, t)` +- Binære variabler +- Tidspunkt `t` passer for person `p` + +`c(k, t)` +- Heltallsvariabel +- Kapasitet for komité `k` på tidspunkt `t` (hvor mange intervju de kan ha på det gitte tidspunktet) + +## Begrensninger + +For alle `p`: + +- `m(p, k, t) <= 1` dersom + - `p` har søkt på komité `k` + - `c(p, t) => 1` + - `c(k, t) => 1` +- `m(p, k, t) <= 0` ellers + +For alle `k`: +- `sum(m(p, k, t)) <= c(k, t)` for alle personer `p` og tidspunkt `t` + + + +## Mål + +Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t` From bde1fc0df2aaeb8dc3a07908f64fde2ff312a5b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 3 Mar 2024 16:59:20 +0100 Subject: [PATCH 05/75] test: add unittests for Applicant, Committee and TimeInterval --- .vscode/settings.json | 13 ++- .../Committee.py | 28 ------ .../Modellering.md | 52 ---------- .../mip_test.py | 70 ------------- .../Applicant.py | 33 +++++-- algorithm/mip_matching/Committee.py | 78 +++++++++++++++ algorithm/mip_matching/TimeInterval.py | 85 ++++++++++++++++ algorithm/mip_matching/__init__.py | 0 algorithm/mip_matching/tests/ApplicantTest.py | 24 +++++ algorithm/mip_matching/tests/CommitteeTest.py | 17 ++++ .../mip_matching/tests/TimeIntervalTest.py | 63 ++++++++++++ algorithm/mip_matching/tests/__init__.py | 0 .../tests}/fixed_test.py | 97 +++---------------- algorithm/mip_matching/tests/mip_test.py | 62 ++++++++++++ 14 files changed, 379 insertions(+), 243 deletions(-) delete mode 100644 algorithm/Mixed Integer Linear Programming/Committee.py delete mode 100644 algorithm/Mixed Integer Linear Programming/Modellering.md delete mode 100644 algorithm/Mixed Integer Linear Programming/mip_test.py rename algorithm/{Mixed Integer Linear Programming => mip_matching}/Applicant.py (62%) create mode 100644 algorithm/mip_matching/Committee.py create mode 100644 algorithm/mip_matching/TimeInterval.py create mode 100644 algorithm/mip_matching/__init__.py create mode 100644 algorithm/mip_matching/tests/ApplicantTest.py create mode 100644 algorithm/mip_matching/tests/CommitteeTest.py create mode 100644 algorithm/mip_matching/tests/TimeIntervalTest.py create mode 100644 algorithm/mip_matching/tests/__init__.py rename algorithm/{Mixed Integer Linear Programming => mip_matching/tests}/fixed_test.py (53%) create mode 100644 algorithm/mip_matching/tests/mip_test.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 9bf4d12b..c648504e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,13 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true -} + "editor.formatOnSave": true, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./algorithm", + "-p", + "*test.py" + ], + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": false, +} \ No newline at end of file diff --git a/algorithm/Mixed Integer Linear Programming/Committee.py b/algorithm/Mixed Integer Linear Programming/Committee.py deleted file mode 100644 index ef338954..00000000 --- a/algorithm/Mixed Integer Linear Programming/Committee.py +++ /dev/null @@ -1,28 +0,0 @@ -from fixed_test import TimeInterval - - -class Committee: - """ - En klasse som representerer en komité - og holder oversikt over når komitéene kan ha - møte og hvor lange intervjuene er. - """ - - def __init__(self, interview_length: int = 1): - self.capacities: dict[TimeInterval, int] = dict() - self.interview_length: int = interview_length - - def add_interval(self, interval: TimeInterval, capacity: int = 1) -> None: - """Legger til et nytt intervall med gitt kapasitet hvis intervallet - ikke allerede har en kapasitet for denne komitéen. - Når intervaller legges til deles det automatisk opp i - intervaller med lik lengde som intervjulengder.""" - minimal_intervals = TimeInterval.divide_interval(interval=interval, length=self.interview_length) - for interval in minimal_intervals: - if interval not in self.capacities: - self.capacities[interval] = capacity - - def add_intervals_with_capacities(self, intervals_with_capacities: dict[TimeInterval, int]): - """Legger til flere tidsintervaller samtidig.""" - for interval, capacity in intervals_with_capacities.items(): - self.add_interval(interval, capacity) diff --git a/algorithm/Mixed Integer Linear Programming/Modellering.md b/algorithm/Mixed Integer Linear Programming/Modellering.md deleted file mode 100644 index 3fea01e0..00000000 --- a/algorithm/Mixed Integer Linear Programming/Modellering.md +++ /dev/null @@ -1,52 +0,0 @@ -# Modellering av problem gjennom Mixed Integer Linear Programming - -## Nyttige ressurser - -- https://python-mip.readthedocs.io/en/latest/quickstart.html -- https://towardsdatascience.com/mixed-integer-linear-programming-1-bc0ef201ee87 -- https://towardsdatascience.com/mixed-integer-linear-programming-formal-definition-and-solution-space-6b3286d54892 -- https://www.gurobi.com/resources/mixed-integer-programming-mip-a-primer-on-the-basics/ - -## Variabler - -`p` -- Person - -`k` -- Komité - -`t` -- Timeslot (Må gjøres til intervaller etter hvert) - -`m(p, k, t)` -- Binær variabel -- Person `p` har møte med komité `k` i timeslot `t` - -## Hjelpevariabler - -`c(p, t)` -- Binære variabler -- Tidspunkt `t` passer for person `p` - -`c(k, t)` -- Heltallsvariabel -- Kapasitet for komité `k` på tidspunkt `t` (hvor mange intervju de kan ha på det gitte tidspunktet) - -## Begrensninger - -For alle `p`: - -- `m(p, k, t) <= 1` dersom - - `p` har søkt på komité `k` - - `c(p, t) => 1` - - `c(k, t) => 1` -- `m(p, k, t) <= 0` ellers - -For alle `k`: -- `sum(m(p, k, t)) <= c(k, t)` for alle personer `p` og tidspunkt `t` - - - -## Mål - -Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t` diff --git a/algorithm/Mixed Integer Linear Programming/mip_test.py b/algorithm/Mixed Integer Linear Programming/mip_test.py deleted file mode 100644 index 08ea7db9..00000000 --- a/algorithm/Mixed Integer Linear Programming/mip_test.py +++ /dev/null @@ -1,70 +0,0 @@ -import mip -import itertools - -from fixed_test import TimeIntervals - -from fixed_test import komiteer_og_tidsslots, komiteer, komite_kapasiteter -from fixed_test import komiteer_per_person, personer, personer_og_tidsslots - -model = mip.Model(sense=mip.MAXIMIZE) - -m = {} - -# Lager alle maksimeringsvariabler -for person in personer: - - for komite in komiteer_per_person[person]: - for person_intervall, komite_intervall in itertools.product(personer_og_tidsslots[person], - komiteer_og_tidsslots[komite]): - # Går gjennom alle kombinasjoner av intervaller for person og for komité - - if person_intervall.contains(komite_intervall): - # Legger til alle intervaller for komitéen som passer for personen - - m[(person, komite, komite_intervall) - ] = model.add_var(var_type=mip.BINARY, name=f"({person}, {komite}, {komite_intervall})") - -print(m) - -# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. -for komite in komiteer: - for slot in komiteer_og_tidsslots[komite]: - model += mip.xsum(m[(person, komite, slot)] - for person in personer if (person, komite, slot) in m) <= komite_kapasiteter[komite][slot] - - -# Legger inn begrensninger for at en person kun har ett intervju med hver komité -for person in personer: - for komite in komiteer: - # if person == "Jørgen": - # print(person, komite) - # print(personer_og_tidsslots[person]) - # print(komiteer_og_tidsslots[komite]) - # print(personer_og_tidsslots[person].intersection( - # komiteer_og_tidsslots[komite])) - - person_tider: TimeIntervals = TimeIntervals(personer_og_tidsslots[person]) - komite_tider: TimeIntervals = TimeIntervals(komiteer_og_tidsslots[komite]) - - model += mip.xsum(m[(person, komite, slot)] for slot in person_tider.recursive_intersection(komite_tider) if komite in komiteer_per_person[person]) <= 1 - -model.objective = mip.maximize(mip.xsum(m.values())) - -solver_status = model.optimize() - - -# Møtetider: -antall_matchede_møter: int = 0 -for name, variable in m.items(): - if variable.x: - antall_matchede_møter += 1 - print(f"{name}: {variable.x}") - -antall_ønskede_møter = sum( - len(komiteer_per_person[person]) for person in personer) - -print( - f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") - - -print(solver_status) diff --git a/algorithm/Mixed Integer Linear Programming/Applicant.py b/algorithm/mip_matching/Applicant.py similarity index 62% rename from algorithm/Mixed Integer Linear Programming/Applicant.py rename to algorithm/mip_matching/Applicant.py index eda06bdc..ee0181b0 100644 --- a/algorithm/Mixed Integer Linear Programming/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -1,18 +1,28 @@ -from Committee import Committee -from fixed_test import TimeInterval +from __future__ import annotations + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + # Unngår cyclic import + from mip_matching.Committee import Committee + from mip_matching.TimeInterval import TimeInterval import itertools + class Applicant: """ Klasse som holder styr over en søker, med data om hvilke - komitéer hen har søkt på, og når søkeren kan ha intervjuer.""" - def __init__(self): - self.committee: list[Committee] = [] - self.slots: set[TimeInterval] = [] + komitéer hen har søkt på, og når søkeren kan ha intervjuer. + """ + + def __init__(self, name: str): + self.committees: list[Committee] = [] + self.slots: set[TimeInterval] = set() + self.name = name def add_committee(self, committee: Committee) -> None: - self.committee.append(committee) + self.committees.append(committee) + committee._add_applicant(self) def add_committees(self, committees: set[Committee]) -> None: for committee in committees: @@ -34,8 +44,15 @@ def get_fitting_committee_slots(self, committee: Committee) -> set[TimeInterval] result: set[TimeInterval] = set() - for applicant_interval, committee_interval in itertools.product(self.slots, committee.slots): + for applicant_interval, committee_interval in itertools.product(self.slots, committee.get_intervals()): if applicant_interval.contains(committee_interval): result.add(committee_interval) return result + + def get_committees(self) -> set[Committee]: + """Returnerer en grunn kopi av komitéene.""" + return set(self.committees) + + def __str__(self) -> str: + return self.name diff --git a/algorithm/mip_matching/Committee.py b/algorithm/mip_matching/Committee.py new file mode 100644 index 00000000..22ba4578 --- /dev/null +++ b/algorithm/mip_matching/Committee.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from mip_matching.Applicant import Applicant + +from typing import Iterator +# from typing import TYPE_CHECKING +# if TYPE_CHECKING: +# # Unngår cyclic import +from mip_matching.TimeInterval import TimeInterval + + +class Committee: + """ + En klasse som representerer en komité + og holder oversikt over når komitéene kan ha + møte og hvor lange intervjuene er. + + NOTE: + - Kan foreløpig kun aksessere ved hjelp av det faktiske + intervallet slik det er inndelt basert på intervju-lengde, + men er usikker på om vi kanskje burde fått med annen måte å + aksessere på. + """ + + def __init__(self, name: str, interview_length: int = 1): + self.capacities: dict[TimeInterval, int] = dict() + self.interview_length: int = interview_length + self.applicants: set[Applicant] = set() + self.name = name + + def add_interval(self, interval: TimeInterval, capacity: int = 1) -> None: + """Legger til et nytt intervall med gitt kapasitet hvis intervallet + ikke allerede har en kapasitet for denne komitéen. + Når intervaller legges til deles det automatisk opp i + intervaller med lik lengde som intervjulengder.""" + minimal_intervals = TimeInterval.divide_interval( + interval=interval, length=self.interview_length) + for interval in minimal_intervals: + if interval not in self.capacities: + self.capacities[interval] = capacity + else: + self.capacities[interval] += capacity + + def add_intervals_with_capacities(self, intervals_with_capacities: dict[TimeInterval, int]): + """Legger til flere tidsintervaller samtidig.""" + for interval, capacity in intervals_with_capacities.items(): + self.add_interval(interval, capacity) + + def get_intervals_and_capacities(self) -> Iterator[tuple[TimeInterval, int]]: + """Generator som returnerer interval-kapasitet-par.""" + for interval, capacity in self.capacities.items(): + yield interval, capacity + + def get_intervals(self) -> Iterator[TimeInterval]: + """Generator som returnerer kun intervallene""" + for interval in self.capacities.keys(): + yield interval + + def _add_applicant(self, applicant: Applicant): + """Metode brukt for å holde toveis-assosiasjonen.""" + self.applicants.add(applicant) + + def get_applicants(self) -> Iterator[Applicant]: + for applicant in self.applicants: + yield applicant + + def get_capacity(self, interval: TimeInterval) -> int: + """Returnerer komitéens kapasitet ved et gitt interval (ferdiginndelt etter lengde)""" + return self.capacities[interval] + + def get_applicant_count(self) -> int: + return len(self.applicants) + + def __str__(self): + return f"{self.name} - {self.get_applicant_count()} applicants" + + def __repr__(self): + return str(self) diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py new file mode 100644 index 00000000..6fcd7b78 --- /dev/null +++ b/algorithm/mip_matching/TimeInterval.py @@ -0,0 +1,85 @@ +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass(frozen=True) +class TimeInterval: + """ + Definerer et tidsintervall fra og med start til og uten end. + """ + start: int + end: int + + def overlaps(self, other: TimeInterval) -> bool: + """Returnerer true om to timeslots er helt eller delvis overlappende.""" + return other.start <= self.start < other.end or self.start <= other.start < self.end + + def contains(self, other: TimeInterval) -> bool: + """Returnerer true om other inngår helt i self.""" + return self.start <= other.start and other.end <= self.end + + def intersection(self, other: TimeInterval) -> TimeInterval: + """Returnerer et snitt av to timeslots.""" + if not self.overlaps(other): + # Snittet er tomt grunnet ingen overlapp + return None + + start = max(self.start, other.start) + end = min(self.end, other.end) + return TimeInterval(start, end) + + def get_contained_slots(self, slots: list[TimeInterval]): + """Returnerer en delmengde av de slots i listen av timeslots + "slots", som inngår helt i dette timeslottet.""" + return set(slot for slot in slots if self.contains(slot)) + + def divide(self, length: int) -> list[TimeInterval]: + return TimeInterval.divide_interval(self, length) + + @staticmethod + def divide_interval(interval: TimeInterval, length: int) -> list[TimeInterval]: + """ + + Deler opp et intervall i mindre intervaller av lengde *length*. + + Note: + - Det antas at intervallet kan deles opp i hele deler av lengde *length*. + Overskytende tid vil bli ignorert. + """ + result = [] + global_start = interval.start + local_start = global_start + local_end = local_start + length + + while local_end <= interval.end: + result.append(TimeInterval(local_start, local_end)) + local_start = local_end + local_end += length + + return result + + +""" +Dette er gammel kode som nå er flyttet til de passende komité-/søker-klassene. +Foreløpig beholdt for referanse. +""" +# class TimeIntervals: +# def __init__(self, initial_list: list[TimeInterval] = None): +# self.list: list[TimeInterval] = initial_list if initial_list else [] + +# def add(self, interval: TimeInterval): +# self.list.append(interval) + +# def recursive_intersection(self, other: TimeIntervals): +# """ +# Returnerer alle tidsintervallene i *other* som er inneholdt i et av *self* sine intervaller""" +# result = TimeIntervals() + +# for self_interval, other_interval in itertools.product(self.list, other.list): +# if self_interval.contains(other_interval): +# result.add(other_interval) + +# return result + +# def __iter__(self): +# return self.list.__iter__() diff --git a/algorithm/mip_matching/__init__.py b/algorithm/mip_matching/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/mip_matching/tests/ApplicantTest.py new file mode 100644 index 00000000..5d99cd06 --- /dev/null +++ b/algorithm/mip_matching/tests/ApplicantTest.py @@ -0,0 +1,24 @@ +from __future__ import annotations +import unittest +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Applicant import Applicant +from mip_matching.Committee import Committee + + +class ApplicantTest(unittest.TestCase): + def setUp(self) -> None: + self.committee = Committee("TestKom", interview_length=2) + self.committee.add_intervals_with_capacities({ + TimeInterval(0, 6): 1, + TimeInterval(4, 6): 1 + }) + + def test_get_fitting_committee_slots(self) -> None: + test_applicant = Applicant("Test Testesen") + + test_applicant.add_interval(TimeInterval(-2, 8)) + + test_applicant.get_fitting_committee_slots(self.committee) + + self.assertEqual(set([TimeInterval(0, 2), TimeInterval(2, 4), TimeInterval( + 4, 6)]), test_applicant.get_fitting_committee_slots(self.committee)) diff --git a/algorithm/mip_matching/tests/CommitteeTest.py b/algorithm/mip_matching/tests/CommitteeTest.py new file mode 100644 index 00000000..64260456 --- /dev/null +++ b/algorithm/mip_matching/tests/CommitteeTest.py @@ -0,0 +1,17 @@ +from __future__ import annotations +import unittest +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Committee import Committee + + +class ApplicantTest(unittest.TestCase): + def setUp(self) -> None: + self.committee = Committee("TestKom", interview_length=2) + self.committee.add_intervals_with_capacities({ + TimeInterval(0, 6): 1, + TimeInterval(2, 6): 1 + }) + + def test_capacity_stacking(self) -> None: + self.assertEqual(1, self.committee.get_capacity(TimeInterval(0, 2))) + self.assertEqual(2, self.committee.get_capacity(TimeInterval(2, 4))) diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/mip_matching/tests/TimeIntervalTest.py new file mode 100644 index 00000000..21646d08 --- /dev/null +++ b/algorithm/mip_matching/tests/TimeIntervalTest.py @@ -0,0 +1,63 @@ +from __future__ import annotations +import unittest +from mip_matching.TimeInterval import TimeInterval +# from mip_matching.Applicant import Applicant +# from mip_matching.Committee import Committee + + +class TimeIntervalTest(unittest.TestCase): + def setUp(self): + self.interval = TimeInterval(0, 6) + + def test_overlapping(self): + interval1: TimeInterval = TimeInterval(0, 2) + interval2: TimeInterval = TimeInterval(1, 3) + + self.assertTrue(interval1.overlaps(interval2)) + + def test_overlapping_edge(self): + interval1: TimeInterval = TimeInterval(0, 1) + interval2: TimeInterval = TimeInterval(1, 2) + + self.assertFalse(interval1.overlaps(interval2)) + + interval3: TimeInterval = TimeInterval(0, 2) + + self.assertTrue(interval1.overlaps(interval3)) + + def test_division(self): + actual_division = self.interval.divide(2) + expected_division = [TimeInterval( + 0, 2), TimeInterval(2, 4), TimeInterval(4, 6)] + + self.assertEqual(expected_division, actual_division) + + def test_contains(self): + self.assertTrue(self.interval.contains(TimeInterval(0, 4))) + self.assertTrue(self.interval.contains(TimeInterval(0, 6))) + self.assertTrue(self.interval.contains(TimeInterval(4, 6))) + self.assertTrue(self.interval.contains(TimeInterval(2, 4))) + + self.assertFalse(self.interval.contains(TimeInterval(-1, 2))) + self.assertFalse(self.interval.contains(TimeInterval(-1, 7))) + self.assertFalse(self.interval.contains(TimeInterval(2, 7))) + + def test_intersection(self): + self.assertEqual(TimeInterval( + 4, 6), self.interval.intersection(TimeInterval(4, 7))) + self.assertEqual(TimeInterval( + 4, 6), self.interval.intersection(TimeInterval(4, 6))) + self.assertEqual(TimeInterval( + 4, 5), self.interval.intersection(TimeInterval(4, 5))) + self.assertEqual(TimeInterval( + 0, 5), self.interval.intersection(TimeInterval(-5, 5))) + + def test_get_contained_slots(self): + test_case_slots = [TimeInterval(-1, 2), TimeInterval(0, 2), + TimeInterval(1, 4), TimeInterval(4, 8), TimeInterval(3, 6)] + actual_contained = self.interval.get_contained_slots(test_case_slots) + expected_contained = [TimeInterval( + 0, 2), TimeInterval(1, 4), TimeInterval(3, 6)] + + self.assertTrue(len(expected_contained), len(actual_contained)) + self.assertEqual(set(expected_contained), set(actual_contained)) diff --git a/algorithm/mip_matching/tests/__init__.py b/algorithm/mip_matching/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/algorithm/Mixed Integer Linear Programming/fixed_test.py b/algorithm/mip_matching/tests/fixed_test.py similarity index 53% rename from algorithm/Mixed Integer Linear Programming/fixed_test.py rename to algorithm/mip_matching/tests/fixed_test.py index 5d7ca480..4c32a7e9 100644 --- a/algorithm/Mixed Integer Linear Programming/fixed_test.py +++ b/algorithm/mip_matching/tests/fixed_test.py @@ -7,118 +7,49 @@ """ from __future__ import annotations -from dataclasses import dataclass -import itertools +import sys +print(sys.path) -from Committee import Committee -from Applicant import Applicant +from mip_matching.Committee import Committee +from mip_matching.Applicant import Applicant +from mip_matching.TimeInterval import TimeInterval -@dataclass(frozen=True) -class TimeInterval: - start: int - end: int - - def overlaps(self, other: TimeInterval) -> bool: - """Returnerer true om to timeslots er helt eller delvis overlappende.""" - return other.start < self.start < other.end or self.start < other.start < self.end - - def contains(self, other: TimeInterval) -> bool: - """Returnerer true om other inngår helt i self.""" - return self.start <= other.start and other.end <= self.end - - def intersection(self, other: TimeInterval) -> TimeInterval: - """Returnerer et snitt av to timeslots.""" - if not self.overlaps(other): - # Snittet er tomt grunnet ingen overlapp - return None - - start = max(self.start, other.start) - end = min(self.end, other.end) - return TimeInterval(start, end) - - def get_contained_slots(self, slots: list[TimeInterval]): - """Returnerer en delmengde av de slots i listen av timeslots - "slots", som inngår helt i dette timeslottet.""" - return set(slot for slot in slots if self.contains(slot)) - - @staticmethod - def divide_interval(interval: TimeInterval, length: int) -> list[TimeInterval]: - """ - - Deler opp et intervall i mindre intervaller av lengde *length*. - - Note: - - Det antas at intervallet kan deles opp i hele deler av lengde *length*. - Overskytende tid vil bli ignorert. - """ - result = [] - global_start = interval.start - local_start = global_start - local_end = local_start + length - while local_end < interval.end: - result.append(TimeInterval(local_start, local_end)) - - return result - -""" -Dette er gammel kode som nå er flyttet til de passende komité-/søker-klassene. -Foreløpig beholdt for referanse. -""" -# class TimeIntervals: -# def __init__(self, initial_list: list[TimeInterval] = None): -# self.list: list[TimeInterval] = initial_list if initial_list else [] - -# def add(self, interval: TimeInterval): -# self.list.append(interval) - -# def recursive_intersection(self, other: TimeIntervals): -# """ -# Returnerer alle tidsintervallene i *other* som er inneholdt i et av *self* sine intervaller""" -# result = TimeIntervals() - -# for self_interval, other_interval in itertools.product(self.list, other.list): -# if self_interval.contains(other_interval): -# result.add(other_interval) - -# return result - -# def __iter__(self): -# return self.list.__iter__() committees: set[Committee] -appkom = Committee() +appkom = Committee(name="Appkom") appkom.add_intervals_with_capacities({TimeInterval(1, 2): 1, TimeInterval(2, 3): 1, TimeInterval(3, 4): 1, TimeInterval(4, 5): 1}) -oil = Committee() +oil = Committee(name="OIL") oil.add_intervals_with_capacities({TimeInterval(4, 5): 1, TimeInterval(5, 6): 1}) -prokom = Committee() +prokom = Committee(name="Prokom") prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, TimeInterval(4, 6): 1}) committees = {appkom, oil, prokom} -jørgen: Applicant = Applicant() +jørgen: Applicant = Applicant(name="Jørgen") jørgen.add_committees({appkom, prokom}) jørgen.add_intervals({TimeInterval(1, 4)}) -sindre: Applicant = Applicant() +sindre: Applicant = Applicant(name="Sindre") sindre.add_committees({appkom, oil}) sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) -julian: Applicant = Applicant() +julian: Applicant = Applicant(name="Julian") julian.add_committees({appkom, prokom, oil}) -julian.add_intervals({TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) +julian.add_intervals( + {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) -fritz: Applicant = Applicant() +fritz: Applicant = Applicant(name="Fritz") fritz.add_committees({oil}) fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py new file mode 100644 index 00000000..c39f4183 --- /dev/null +++ b/algorithm/mip_matching/tests/mip_test.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from mip_matching.tests.fixed_test import applicants, committees +import mip + +import unittest + + +model = mip.Model(sense=mip.MAXIMIZE) + +m = {} + +# Lager alle maksimeringsvariabler +for applicant in applicants: + print(applicant) + for committee in applicant.get_committees(): + for interval in applicant.get_fitting_committee_slots(committee): + m[(applicant, committee, interval)] = model.add_var( + var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") + +print(m) + +# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. +for committee in committees: + for interval, capacity in committee.get_intervals_and_capacities(): + model += mip.xsum(m[(applicant, committee, interval)] + for applicant in committee.get_applicants() + if (applicant, committee, interval) in m) <= capacity + + +# Legger inn begrensninger for at en person kun har ett intervju med hver komité +for applicant in applicants: + for committee in applicant.get_committees(): + model += mip.xsum(m[(applicant, committee, interval)] + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + +model.objective = mip.maximize(mip.xsum(m.values())) + +# Kjør optimeringen +solver_status = model.optimize() + + +# Få de faktiske møtetidene +antall_matchede_møter: int = 0 +for name, variable in m.items(): + if variable.x: + antall_matchede_møter += 1 + print(f"{name}: {variable.x}") + +antall_ønskede_møter = sum( + len(applicant.get_committees()) for applicant in applicants) + +print( + f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") + + +print(solver_status) + + +class TestTest(unittest.TestCase): + def test_test(self): + self.assertTrue(True) \ No newline at end of file From 948a0a973b5b06943b811e4076df54358dfb9227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 13:27:59 +0100 Subject: [PATCH 06/75] fix: made debugging easier --- algorithm/mip_matching/Applicant.py | 3 +++ algorithm/mip_matching/Committee.py | 2 +- algorithm/mip_matching/tests/mip_test.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/mip_matching/Applicant.py index ee0181b0..85ed4bb8 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -56,3 +56,6 @@ def get_committees(self) -> set[Committee]: def __str__(self) -> str: return self.name + + def __repr__(self) -> str: + return str(self) diff --git a/algorithm/mip_matching/Committee.py b/algorithm/mip_matching/Committee.py index 22ba4578..e81b1645 100644 --- a/algorithm/mip_matching/Committee.py +++ b/algorithm/mip_matching/Committee.py @@ -72,7 +72,7 @@ def get_applicant_count(self) -> int: return len(self.applicants) def __str__(self): - return f"{self.name} - {self.get_applicant_count()} applicants" + return f"{self.name}" def __repr__(self): return str(self) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index c39f4183..fa8601f7 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -45,7 +45,7 @@ for name, variable in m.items(): if variable.x: antall_matchede_møter += 1 - print(f"{name}: {variable.x}") + print(f"{name}") antall_ønskede_møter = sum( len(applicant.get_committees()) for applicant in applicants) @@ -59,4 +59,4 @@ class TestTest(unittest.TestCase): def test_test(self): - self.assertTrue(True) \ No newline at end of file + self.assertTrue(True) From ffe7e8a3b65afed270e27ac0bf2fcd58af85b18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 14:40:46 +0100 Subject: [PATCH 07/75] refactor(algorithm): :recycle: move fixed test to test framework --- .vscode/settings.json | 3 + algorithm/mip_matching/tests/fixed_test.py | 47 +------ algorithm/mip_matching/tests/mip_test.py | 137 +++++++++++++++------ 3 files changed, 105 insertions(+), 82 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c648504e..d058f362 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,4 +10,7 @@ ], "python.testing.unittestEnabled": true, "python.testing.pytestEnabled": false, + "conventionalCommits.scopes": [ + "algorithm" + ], } \ No newline at end of file diff --git a/algorithm/mip_matching/tests/fixed_test.py b/algorithm/mip_matching/tests/fixed_test.py index 4c32a7e9..043c2b6e 100644 --- a/algorithm/mip_matching/tests/fixed_test.py +++ b/algorithm/mip_matching/tests/fixed_test.py @@ -7,54 +7,13 @@ """ from __future__ import annotations +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Applicant import Applicant +from mip_matching.Committee import Committee import sys print(sys.path) -from mip_matching.Committee import Committee -from mip_matching.Applicant import Applicant -from mip_matching.TimeInterval import TimeInterval - - - - -committees: set[Committee] - -appkom = Committee(name="Appkom") -appkom.add_intervals_with_capacities({TimeInterval(1, 2): 1, - TimeInterval(2, 3): 1, - TimeInterval(3, 4): 1, - TimeInterval(4, 5): 1}) - -oil = Committee(name="OIL") -oil.add_intervals_with_capacities({TimeInterval(4, 5): 1, - TimeInterval(5, 6): 1}) - -prokom = Committee(name="Prokom") -prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, - TimeInterval(4, 6): 1}) - -committees = {appkom, oil, prokom} - -jørgen: Applicant = Applicant(name="Jørgen") -jørgen.add_committees({appkom, prokom}) -jørgen.add_intervals({TimeInterval(1, 4)}) - -sindre: Applicant = Applicant(name="Sindre") -sindre.add_committees({appkom, oil}) -sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) - -julian: Applicant = Applicant(name="Julian") -julian.add_committees({appkom, prokom, oil}) -julian.add_intervals( - {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) - -fritz: Applicant = Applicant(name="Fritz") -fritz.add_committees({oil}) -fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) - -applicants: set[Applicant] = {jørgen, sindre, julian, fritz} - # personer_og_tidsslots = {"Jørgen": {TimeInterval(1, 4)}, # "Sindre": {TimeInterval(2, 3), TimeInterval(4, 6)}, diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index fa8601f7..03626246 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -1,62 +1,123 @@ from __future__ import annotations +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Committee import Committee from mip_matching.tests.fixed_test import applicants, committees +from mip_matching.Applicant import Applicant import mip +from typing import TypedDict + import unittest -model = mip.Model(sense=mip.MAXIMIZE) +class MeetingMatch(TypedDict): + """Type definition of a meeting match object""" + solver_status: mip.OptimizationStatus + matched_meetings: int + total_wanted_meetings: int + matchings: list[tuple[Applicant, Committee, TimeInterval]] + + +def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: + """Matches meetings and returns a MeetingMatch-object""" + model = mip.Model(sense=mip.MAXIMIZE) + + m = {} + + # Lager alle maksimeringsvariabler + for applicant in applicants: + for committee in applicant.get_committees(): + for interval in applicant.get_fitting_committee_slots(committee): + m[(applicant, committee, interval)] = model.add_var( + var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") + + # Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. + for committee in committees: + for interval, capacity in committee.get_intervals_and_capacities(): + model += mip.xsum(m[(applicant, committee, interval)] + for applicant in committee.get_applicants() + if (applicant, committee, interval) in m) <= capacity + + # Legger inn begrensninger for at en person kun har ett intervju med hver komité + for applicant in applicants: + for committee in applicant.get_committees(): + model += mip.xsum(m[(applicant, committee, interval)] + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + + # Setter mål til å være maksimering av antall møter + model.objective = mip.maximize(mip.xsum(m.values())) + + # Kjør optimeringen + solver_status = model.optimize() + + # Få de faktiske møtetidene + antall_matchede_møter: int = 0 + matchings: list = [] + for name, variable in m.items(): + if variable.x: + antall_matchede_møter += 1 + matchings.append(name) + print(f"{name}") + + antall_ønskede_møter = sum( + len(applicant.get_committees()) for applicant in applicants) + + match_object: MeetingMatch = { + "solver_status": solver_status, + "matched_meetings": antall_matchede_møter, + "total_wanted_meetings": antall_ønskede_møter, + "matchings": matchings, + } + + return match_object -m = {} -# Lager alle maksimeringsvariabler -for applicant in applicants: - print(applicant) - for committee in applicant.get_committees(): - for interval in applicant.get_fitting_committee_slots(committee): - m[(applicant, committee, interval)] = model.add_var( - var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") +class MipTest(unittest.TestCase): + def test_fixed_small(self): + """Small, fixed test with all capacities set to one""" -print(m) + appkom = Committee(name="Appkom") + appkom.add_intervals_with_capacities({TimeInterval(1, 5): 1}) -# Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. -for committee in committees: - for interval, capacity in committee.get_intervals_and_capacities(): - model += mip.xsum(m[(applicant, committee, interval)] - for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity + oil = Committee(name="OIL") + oil.add_intervals_with_capacities({TimeInterval(4, 6): 1}) + prokom = Committee(name="Prokom") + prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, + TimeInterval(4, 6): 1}) -# Legger inn begrensninger for at en person kun har ett intervju med hver komité -for applicant in applicants: - for committee in applicant.get_committees(): - model += mip.xsum(m[(applicant, committee, interval)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + committees: set[Committee] = {appkom, oil, prokom} -model.objective = mip.maximize(mip.xsum(m.values())) + jørgen: Applicant = Applicant(name="Jørgen") + jørgen.add_committees({appkom, prokom}) + jørgen.add_intervals({TimeInterval(1, 4)}) -# Kjør optimeringen -solver_status = model.optimize() + sindre: Applicant = Applicant(name="Sindre") + sindre.add_committees({appkom, oil}) + sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) + julian: Applicant = Applicant(name="Julian") + julian.add_committees({appkom, prokom, oil}) + julian.add_intervals( + {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) -# Få de faktiske møtetidene -antall_matchede_møter: int = 0 -for name, variable in m.items(): - if variable.x: - antall_matchede_møter += 1 - print(f"{name}") + fritz: Applicant = Applicant(name="Fritz") + fritz.add_committees({oil}) + fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) -antall_ønskede_møter = sum( - len(applicant.get_committees()) for applicant in applicants) + applicants: set[Applicant] = {jørgen, sindre, julian, fritz} -print( - f"Klarte å matche {antall_matchede_møter} av {antall_ønskede_møter} ({antall_matchede_møter/antall_ønskede_møter:2f})") + match = match_meetings(applicants=applicants, committees=committees) + # Expectations + expected_number_of_matched_meetings = 7 -print(solver_status) + print( + f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") + self.assertEqual(expected_number_of_matched_meetings, + match["matched_meetings"]) -class TestTest(unittest.TestCase): - def test_test(self): - self.assertTrue(True) + def test_randomized_large(self): + \ No newline at end of file From 18a85759435db3de061dd162fff70a7afae2aa42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:18:27 +0100 Subject: [PATCH 08/75] config: add standard type checking for python --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index d058f362..f780532c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "-p", "*test.py" ], + "python.analysis.typeCheckingMode": "standard", "python.testing.unittestEnabled": true, "python.testing.pytestEnabled": false, "conventionalCommits.scopes": [ From d7fe7d5f983c14726fb242e7c04ef2f94af4079a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:30:23 +0100 Subject: [PATCH 09/75] fix: add constraint "only one interview per slot per person" --- algorithm/mip_matching/tests/mip_test.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 03626246..f7adbc23 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -37,13 +37,20 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me for interval, capacity in committee.get_intervals_and_capacities(): model += mip.xsum(m[(applicant, committee, interval)] for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity + if (applicant, committee, interval) in m) <= capacity # type: ignore # Legger inn begrensninger for at en person kun har ett intervju med hver komité for applicant in applicants: for committee in applicant.get_committees(): model += mip.xsum(m[(applicant, committee, interval)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore + + # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt + for applicant in applicants: + for interval in applicant.get_intervals(): + model += mip.xsum(m[(applicant, committee, interval)] + for committee in applicant.get_committees() + if (applicant, committee, interval) in m) <= 1 # type: ignore # Setter mål til å være maksimering av antall møter model.objective = mip.maximize(mip.xsum(m.values())) From 22ebe24df216f21f9eef0a36a08f3025387c50c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:32:01 +0100 Subject: [PATCH 10/75] test: add constraint test --- algorithm/mip_matching/tests/mip_test.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index f7adbc23..391d9b90 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -81,6 +81,29 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me class MipTest(unittest.TestCase): + def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInterval]]): + """Checks if the constraints are satisfied in the provided matchings. + TODO: Add more constraint tests.""" + + self.assertEqual(len(matchings), len(set((applicant, interval) + for applicant, _, interval in matchings)), + "Constraint \"Applicant can only have one meeting during each TimeInterval\" failed.") + + load_per_committee_per_slot: dict[Committee, dict[TimeInterval, int]] = { + } + for _, committee, interval in matchings: + if committee not in load_per_committee_per_slot: + load_per_committee_per_slot[committee] = {} + + # Øker antall med 1, eller setter inn 1 + load_per_committee_per_slot[committee][interval] = load_per_committee_per_slot[committee].get( + interval, 0) + 1 + + for committee, load_per_interval in load_per_committee_per_slot.items(): + for interval, load in load_per_interval.items(): + self.assertGreaterEqual(committee.get_capacity(interval), load, + f"Constraint \"Number of interviews per slot per committee cannot exceed capacity\" failed for Committee {committee} and interval {interval}") + def test_fixed_small(self): """Small, fixed test with all capacities set to one""" @@ -126,5 +149,7 @@ def test_fixed_small(self): self.assertEqual(expected_number_of_matched_meetings, match["matched_meetings"]) + self.check_constraints(matchings=match["matchings"]) + def test_randomized_large(self): \ No newline at end of file From 39101002a9e753f8e6adf7cc4e6ea4b813f23888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 17 Mar 2024 17:33:44 +0100 Subject: [PATCH 11/75] test: add large randomized test --- algorithm/README.md | 7 +++ algorithm/mip_matching/Applicant.py | 3 + algorithm/mip_matching/tests/fixed_test.py | 67 ---------------------- algorithm/mip_matching/tests/mip_test.py | 58 ++++++++++++++++++- 4 files changed, 66 insertions(+), 69 deletions(-) delete mode 100644 algorithm/mip_matching/tests/fixed_test.py diff --git a/algorithm/README.md b/algorithm/README.md index af9f9141..4cb42e83 100644 --- a/algorithm/README.md +++ b/algorithm/README.md @@ -15,3 +15,10 @@ Lag så en fil i `.\.venv\Lib\site-packages` som slutter på `.pth` og inneholde .\.venv\Scripts\activate python -m pip install -r requirements.txt ``` + +## TODOs + +- [x] Lage funksjon som deler opp fra en komités slot +- [x] Sette opp begrensningene fra modelleringen +- [ ] Flikke litt på modelleringen. +- [ ] Finn ut hvordan man kan preprosessere dataen for å få ned kjøretiden (f. eks ved å lage lister av personer for hver komité.) diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/mip_matching/Applicant.py index 85ed4bb8..a09abf39 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -36,6 +36,9 @@ def add_intervals(self, intervals: set[TimeInterval]) -> None: for interval in intervals: self.add_interval(interval) + def get_intervals(self) -> set[TimeInterval]: + return self.slots.copy() + def get_fitting_committee_slots(self, committee: Committee) -> set[TimeInterval]: """ Returnerer alle tidsintervallene i *komiteen* diff --git a/algorithm/mip_matching/tests/fixed_test.py b/algorithm/mip_matching/tests/fixed_test.py deleted file mode 100644 index 043c2b6e..00000000 --- a/algorithm/mip_matching/tests/fixed_test.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -TODO: -- [Gjort] Lage funksjon som deler opp fra en komités slot -- Sette opp begrensningene fra modelleringen -- Flikke litt på modelleringen. -- Finn ut hvordan man kan preprosessere dataen for å få ned kjøretiden (f. eks ved å lage lister av personer for hver komité.) -""" - -from __future__ import annotations -from mip_matching.TimeInterval import TimeInterval -from mip_matching.Applicant import Applicant -from mip_matching.Committee import Committee - -import sys -print(sys.path) - - -# personer_og_tidsslots = {"Jørgen": {TimeInterval(1, 4)}, -# "Sindre": {TimeInterval(2, 3), TimeInterval(4, 6)}, -# "Julian": {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}, -# "Fritz": {TimeInterval(1, 2), TimeInterval(4, 5)}} -# komiteer_og_tidsslots = {"Appkom": {TimeInterval(1, 2), TimeInterval(2, 3), TimeInterval(3, 4), TimeInterval(4, 5)}, -# "OIL": {TimeInterval(4, 5), TimeInterval(5, 6)}, -# "Prokom": {TimeInterval(1, 3), TimeInterval(4, 6)}} - -# komite_kapasiteter = {"Appkom": {TimeInterval(1, 2): 1, -# TimeInterval(2, 3): 1, -# TimeInterval(3, 4): 1, -# TimeInterval(4, 5): 1}, -# "OIL": {TimeInterval(4, 5): 1, -# TimeInterval(5, 6): 1}, -# "Prokom": {TimeInterval(1, 3): 1, -# TimeInterval(4, 6): 1}} - - -# personer = personer_og_tidsslots.keys() -# komiteer = komiteer_og_tidsslots.keys() - -# intervjulengder = {"Appkom": 1, -# "Prokom": 2, -# "OIL": 1} - -# komiteer_per_person = {"Jørgen": {"Appkom", "Prokom"}, -# "Sindre": {"Appkom", "OIL"}, -# "Julian": {"Appkom", "Prokom", "OIL"}, -# "Fritz": {"OIL"}} - - -""" -Tror det følgende er gammel kode som ble brukt for CSP. -""" - -# # Variables -# variables = set() -# for person in komiteer_per_person: -# for komite in komiteer_per_person[person]: -# variables.add((person, komite)) -# # Domains -# domains = {var: personer_og_tidsslots[var[0]].intersection( -# komiteer_og_tidsslots[var[1]]) for var in variables} - -# constraints = {var: set() for var in variables} -# for var, annen in itertools.permutations(variables, 2): -# if annen[0] == var[0]: -# constraints[var].add(annen) -# if annen[1] == var[1]: -# constraints[var].add(annen) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 391d9b90..f38557e1 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -2,7 +2,6 @@ from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee -from mip_matching.tests.fixed_test import applicants, committees from mip_matching.Applicant import Applicant import mip @@ -152,4 +151,59 @@ def test_fixed_small(self): self.check_constraints(matchings=match["matchings"]) def test_randomized_large(self): - \ No newline at end of file + """ + Tests a randomized selection of applicants, committees and slots. + All committees have a capacity of one. + + This test is without asserts, and mostly to test performance. + """ + + import random + + START_TID = 0 + # Hadde -1 her, men husker ikke hvorfor, så har fjernet det inntil videre. + SLUTT_TID = 10*5*2 + ANTALL_PERSONER = 400 + + ANTALL_SLOTS_PER_PERSON_MIN = 10 + ANTALL_SLOTS_PER_PERSON_MAKS = 20 + + ANTALL_SLOTS_PER_KOMITE_MIN = 5*5*2 + ANTALL_SLOTS_PER_KOMITE_MAKS = 8*5*2 + + ANTALL_KOMITEER_PER_PERSON_MIN = 1 + ANTALL_KOMITEER_PER_PERSON_MAKS = 3 + + komite_navn = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", + "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} + committees: set[Committee] = { + Committee(name=navn) for navn in komite_navn} + + # Gir tider til hver søker + applicants: set[Applicant] = set() + for person in range(ANTALL_PERSONER): + applicant = Applicant(name=str(person)) + # Velger ut et tilfeldig antall slots (alle av lengde 1) innenfor boundsene. + applicant.add_intervals(set(TimeInterval(start_tid, start_tid+1) for start_tid in set((random.sample(range( + START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))))) + + applicants.add(applicant) + + # Gir intervaller til hver komité. + for committee in committees: + committee.add_intervals_with_capacities({TimeInterval(start_tid, start_tid + 1): 1 for start_tid in (random.sample(range( + START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS)))}) + + # Lar hver søker søke på tilfeldige komiteer + committees_list = list(committees) + # Må ha liste for at random.sample skal kunne velge ut riktig + for applicant in applicants: + applicant.add_committees(set(random.sample(committees_list, random.randint( + ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) # type: ignore + + match = match_meetings(applicants=applicants, committees=committees) + self.check_constraints(matchings=match["matchings"]) + + print( + f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") + print(f"Solver status: {match['solver_status']}") From 9aa14c290468bd0730ec2a19042c3dfcdefdc296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 14 Apr 2024 15:26:25 +0200 Subject: [PATCH 12/75] config: la til requirements for venv --- algorithm/requirements.txt | Bin 0 -> 90 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 algorithm/requirements.txt diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a910af925a406ea85a4f357cf4d7689b597af9b5 GIT binary patch literal 90 zcmezWFPR|?2s0UMfzXga4~R{{BrgLOLoQIf08Pe#0VGqvP|1)CBoi5m7>XHEfh0)3 M5m2WQ&;*bQ0KzB@asU7T literal 0 HcmV?d00001 From 8a70f1330d4e3aab57f54ffda96acc5092a3ad41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 14 Apr 2024 17:41:47 +0200 Subject: [PATCH 13/75] fix: skrevet om til at timeInterval bruker datetime --- algorithm/mip_matching/Committee.py | 5 +- algorithm/mip_matching/TimeInterval.py | 26 ++++-- algorithm/mip_matching/tests/ApplicantTest.py | 20 +++-- algorithm/mip_matching/tests/CommitteeTest.py | 14 +-- .../mip_matching/tests/TimeIntervalTest.py | 85 +++++++++++++------ 5 files changed, 104 insertions(+), 46 deletions(-) diff --git a/algorithm/mip_matching/Committee.py b/algorithm/mip_matching/Committee.py index e81b1645..9a43d7c7 100644 --- a/algorithm/mip_matching/Committee.py +++ b/algorithm/mip_matching/Committee.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import timedelta from mip_matching.Applicant import Applicant @@ -22,9 +23,9 @@ class Committee: aksessere på. """ - def __init__(self, name: str, interview_length: int = 1): + def __init__(self, name: str, interview_length: timedelta = timedelta(minutes=15)): self.capacities: dict[TimeInterval, int] = dict() - self.interview_length: int = interview_length + self.interview_length: timedelta = interview_length self.applicants: set[Applicant] = set() self.name = name diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py index 6fcd7b78..0c8296ca 100644 --- a/algorithm/mip_matching/TimeInterval.py +++ b/algorithm/mip_matching/TimeInterval.py @@ -1,5 +1,8 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime +from datetime import timedelta +from typing import Any @dataclass(frozen=True) @@ -7,19 +10,24 @@ class TimeInterval: """ Definerer et tidsintervall fra og med start til og uten end. """ - start: int - end: int + start: datetime + end: datetime + + def __post_init__(self) -> None: + """Metode som sikrer at start og end er av type datetime.""" + if not (isinstance(self.start, datetime) and isinstance(self.end, datetime)): + raise ValueError("Start and end must be of type datetime.") def overlaps(self, other: TimeInterval) -> bool: - """Returnerer true om to timeslots er helt eller delvis overlappende.""" + """Returnerer true om to tidsintervaller er helt eller delvis overlappende.""" return other.start <= self.start < other.end or self.start <= other.start < self.end def contains(self, other: TimeInterval) -> bool: """Returnerer true om other inngår helt i self.""" return self.start <= other.start and other.end <= self.end - def intersection(self, other: TimeInterval) -> TimeInterval: - """Returnerer et snitt av to timeslots.""" + def intersection(self, other: TimeInterval) -> TimeInterval | None: + """Returnerer et snitt av to tidsintervaller.""" if not self.overlaps(other): # Snittet er tomt grunnet ingen overlapp return None @@ -29,15 +37,15 @@ def intersection(self, other: TimeInterval) -> TimeInterval: return TimeInterval(start, end) def get_contained_slots(self, slots: list[TimeInterval]): - """Returnerer en delmengde av de slots i listen av timeslots - "slots", som inngår helt i dette timeslottet.""" + """Returnerer en delmengde av de intervaller i listen + "slots", som inngår helt i dette tidsintervallet.""" return set(slot for slot in slots if self.contains(slot)) - def divide(self, length: int) -> list[TimeInterval]: + def divide(self, length: timedelta) -> list[TimeInterval]: return TimeInterval.divide_interval(self, length) @staticmethod - def divide_interval(interval: TimeInterval, length: int) -> list[TimeInterval]: + def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInterval]: """ Deler opp et intervall i mindre intervaller av lengde *length*. diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/mip_matching/tests/ApplicantTest.py index 5d99cd06..cf66737c 100644 --- a/algorithm/mip_matching/tests/ApplicantTest.py +++ b/algorithm/mip_matching/tests/ApplicantTest.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import datetime, timedelta import unittest from mip_matching.TimeInterval import TimeInterval from mip_matching.Applicant import Applicant @@ -7,18 +8,25 @@ class ApplicantTest(unittest.TestCase): def setUp(self) -> None: - self.committee = Committee("TestKom", interview_length=2) + self.committee = Committee( + "TestKom", interview_length=timedelta(minutes=30)) self.committee.add_intervals_with_capacities({ - TimeInterval(0, 6): 1, - TimeInterval(4, 6): 1 + TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 30)): 1, + TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1 }) def test_get_fitting_committee_slots(self) -> None: test_applicant = Applicant("Test Testesen") - test_applicant.add_interval(TimeInterval(-2, 8)) + test_applicant.add_interval(TimeInterval(datetime(2024, 8, 24, 7, 30), + datetime(2024, 8, 24, 10, 0))) test_applicant.get_fitting_committee_slots(self.committee) - self.assertEqual(set([TimeInterval(0, 2), TimeInterval(2, 4), TimeInterval( - 4, 6)]), test_applicant.get_fitting_committee_slots(self.committee)) + self.assertEqual(set([TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 8, 30), + datetime(2024, 8, 24, 9, 0)), + TimeInterval(datetime(2024, 8, 24, 9, 0), + datetime(2024, 8, 24, 9, 30))]), + test_applicant.get_fitting_committee_slots(self.committee)) diff --git a/algorithm/mip_matching/tests/CommitteeTest.py b/algorithm/mip_matching/tests/CommitteeTest.py index 64260456..4d63ac24 100644 --- a/algorithm/mip_matching/tests/CommitteeTest.py +++ b/algorithm/mip_matching/tests/CommitteeTest.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import datetime, timedelta import unittest from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee @@ -6,12 +7,15 @@ class ApplicantTest(unittest.TestCase): def setUp(self) -> None: - self.committee = Committee("TestKom", interview_length=2) + self.committee = Committee( + "TestKom", interview_length=timedelta(minutes=30)) self.committee.add_intervals_with_capacities({ - TimeInterval(0, 6): 1, - TimeInterval(2, 6): 1 + TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 30)): 1, + TimeInterval(datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 30)): 1 }) def test_capacity_stacking(self) -> None: - self.assertEqual(1, self.committee.get_capacity(TimeInterval(0, 2))) - self.assertEqual(2, self.committee.get_capacity(TimeInterval(2, 4))) + self.assertEqual(1, self.committee.get_capacity( + TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)))) + self.assertEqual(2, self.committee.get_capacity( + TimeInterval(datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 0)))) diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/mip_matching/tests/TimeIntervalTest.py index 21646d08..fa68d29d 100644 --- a/algorithm/mip_matching/tests/TimeIntervalTest.py +++ b/algorithm/mip_matching/tests/TimeIntervalTest.py @@ -1,63 +1,100 @@ from __future__ import annotations import unittest from mip_matching.TimeInterval import TimeInterval +from datetime import datetime +from datetime import timedelta # from mip_matching.Applicant import Applicant # from mip_matching.Committee import Committee class TimeIntervalTest(unittest.TestCase): def setUp(self): - self.interval = TimeInterval(0, 6) + self.interval = TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 9, 30)) def test_overlapping(self): - interval1: TimeInterval = TimeInterval(0, 2) - interval2: TimeInterval = TimeInterval(1, 3) + + interval1: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)) + interval2: TimeInterval = TimeInterval( + datetime(2024, 8, 24, 8, 15), + datetime(2024, 8, 24, 8, 45)) self.assertTrue(interval1.overlaps(interval2)) def test_overlapping_edge(self): - interval1: TimeInterval = TimeInterval(0, 1) - interval2: TimeInterval = TimeInterval(1, 2) + interval1: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 15)) + interval2: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 15), + datetime(2024, 8, 24, 8, 30)) self.assertFalse(interval1.overlaps(interval2)) - interval3: TimeInterval = TimeInterval(0, 2) + interval3: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)) self.assertTrue(interval1.overlaps(interval3)) def test_division(self): - actual_division = self.interval.divide(2) - expected_division = [TimeInterval( - 0, 2), TimeInterval(2, 4), TimeInterval(4, 6)] + actual_division = self.interval.divide(timedelta(minutes=30)) + expected_division = [TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 8, 30), + datetime(2024, 8, 24, 9, 0)), + TimeInterval(datetime(2024, 8, 24, 9, 0), + datetime(2024, 8, 24, 9, 30))] self.assertEqual(expected_division, actual_division) def test_contains(self): - self.assertTrue(self.interval.contains(TimeInterval(0, 4))) - self.assertTrue(self.interval.contains(TimeInterval(0, 6))) - self.assertTrue(self.interval.contains(TimeInterval(4, 6))) - self.assertTrue(self.interval.contains(TimeInterval(2, 4))) + self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 9, 0)))) + self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 9, 30)))) + self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 9, 0), + datetime(2024, 8, 24, 9, 30)))) + self.assertTrue(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 30), + datetime(2024, 8, 24, 9, 0)))) - self.assertFalse(self.interval.contains(TimeInterval(-1, 2))) - self.assertFalse(self.interval.contains(TimeInterval(-1, 7))) - self.assertFalse(self.interval.contains(TimeInterval(2, 7))) + self.assertFalse(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 7, 45), + datetime(2024, 8, 24, 8, 30)))) + self.assertFalse(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 7, 45), + datetime(2024, 8, 24, 9, 31)))) + self.assertFalse(self.interval.contains(TimeInterval(datetime(2024, 8, 24, 8, 30), + datetime(2024, 8, 24, 9, 31)))) def test_intersection(self): self.assertEqual(TimeInterval( - 4, 6), self.interval.intersection(TimeInterval(4, 7))) + datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)), + self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 45)))) self.assertEqual(TimeInterval( - 4, 6), self.interval.intersection(TimeInterval(4, 6))) + datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)), + self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)))) self.assertEqual(TimeInterval( - 4, 5), self.interval.intersection(TimeInterval(4, 5))) + datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 15)), + self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 15)))) self.assertEqual(TimeInterval( - 0, 5), self.interval.intersection(TimeInterval(-5, 5))) + datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 15)), + self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 6, 45), datetime(2024, 8, 24, 9, 15)))) def test_get_contained_slots(self): - test_case_slots = [TimeInterval(-1, 2), TimeInterval(0, 2), - TimeInterval(1, 4), TimeInterval(4, 8), TimeInterval(3, 6)] + test_case_slots = [TimeInterval(datetime(2024, 8, 24, 7, 45), + datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 8, 15), + datetime(2024, 8, 24, 9, 0)), + TimeInterval(datetime(2024, 8, 24, 9, 0), + datetime(2024, 8, 24, 10, 0)), + TimeInterval(datetime(2024, 8, 24, 8, 45), + datetime(2024, 8, 24, 9, 30))] actual_contained = self.interval.get_contained_slots(test_case_slots) - expected_contained = [TimeInterval( - 0, 2), TimeInterval(1, 4), TimeInterval(3, 6)] + expected_contained = [TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 8, 15), + datetime(2024, 8, 24, 9, 0)), + TimeInterval(datetime(2024, 8, 24, 8, 45), + datetime(2024, 8, 24, 9, 30))] self.assertTrue(len(expected_contained), len(actual_contained)) self.assertEqual(set(expected_contained), set(actual_contained)) From 66ac457d0683ab81a7d15bf510ae54b4ba8fbbb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 14 Apr 2024 17:42:59 +0200 Subject: [PATCH 14/75] =?UTF-8?q?fix:=20fikset=20constraint=20for=20at=20e?= =?UTF-8?q?n=20person=20kun=20kan=20ha=20ett=20intervju=20p=C3=A5=20hvert?= =?UTF-8?q?=20tidspunkt=20og=20refactoret=20litt=20kode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algorithm/mip_matching/match_meetings.py | 88 ++++++++++++ algorithm/mip_matching/tests/mip_test.py | 166 +++++++++++------------ 2 files changed, 165 insertions(+), 89 deletions(-) create mode 100644 algorithm/mip_matching/match_meetings.py diff --git a/algorithm/mip_matching/match_meetings.py b/algorithm/mip_matching/match_meetings.py new file mode 100644 index 00000000..b8505eaa --- /dev/null +++ b/algorithm/mip_matching/match_meetings.py @@ -0,0 +1,88 @@ +from typing import TypedDict + +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Committee import Committee +from mip_matching.Applicant import Applicant +import mip + +from typing import TypedDict + + +class MeetingMatch(TypedDict): + """Type definition of a meeting match object""" + solver_status: mip.OptimizationStatus + matched_meetings: int + total_wanted_meetings: int + matchings: list[tuple[Applicant, Committee, TimeInterval]] + + +def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: + """Matches meetings and returns a MeetingMatch-object""" + model = mip.Model(sense=mip.MAXIMIZE) + + m = {} + + # Lager alle maksimeringsvariabler + for applicant in applicants: + for committee in applicant.get_committees(): + for interval in applicant.get_fitting_committee_slots(committee): + m[(applicant, committee, interval)] = model.add_var( + var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") + + # Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. + for committee in committees: + for interval, capacity in committee.get_intervals_and_capacities(): + model += mip.xsum(m[(applicant, committee, interval)] + for applicant in committee.get_applicants() + if (applicant, committee, interval) in m) <= capacity # type: ignore + + # Legger inn begrensninger for at en person kun har ett intervju med hver komité + for applicant in applicants: + for committee in applicant.get_committees(): + model += mip.xsum(m[(applicant, committee, interval)] + for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore + + # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt + for applicant in applicants: + potential_intervals = set() + for applicant_candidate, committee, interval in m: + if applicant == applicant_candidate: + potential_intervals.add(interval) + + for interval in potential_intervals: + + model += mip.xsum(m[(applicant, committee, interval)] + for committee in applicant.get_committees() + if (applicant, committee, interval) in m) <= 1 # type: ignore + + # print(f"{applicant}, {interval}:") + # for committee in applicant.get_committees(): + # if (applicant, committee, interval) in m: + # print(f"\t {(applicant, committee, interval)}") + + # Setter mål til å være maksimering av antall møter + model.objective = mip.maximize(mip.xsum(m.values())) + + # Kjør optimeringen + solver_status = model.optimize() + + # Få de faktiske møtetidene + antall_matchede_møter: int = 0 + matchings: list = [] + for name, variable in m.items(): + if variable.x: + antall_matchede_møter += 1 + matchings.append(name) + print(f"{name}") + + antall_ønskede_møter = sum( + len(applicant.get_committees()) for applicant in applicants) + + match_object: MeetingMatch = { + "solver_status": solver_status, + "matched_meetings": antall_matchede_møter, + "total_wanted_meetings": antall_ønskede_møter, + "matchings": matchings, + } + + return match_object diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index f38557e1..1eb8cd3a 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -1,89 +1,48 @@ from __future__ import annotations +from datetime import datetime, timedelta from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee from mip_matching.Applicant import Applicant import mip +from mip_matching.match_meetings import match_meetings + from typing import TypedDict import unittest -class MeetingMatch(TypedDict): - """Type definition of a meeting match object""" - solver_status: mip.OptimizationStatus - matched_meetings: int - total_wanted_meetings: int - matchings: list[tuple[Applicant, Committee, TimeInterval]] - - -def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: - """Matches meetings and returns a MeetingMatch-object""" - model = mip.Model(sense=mip.MAXIMIZE) - - m = {} - - # Lager alle maksimeringsvariabler - for applicant in applicants: - for committee in applicant.get_committees(): - for interval in applicant.get_fitting_committee_slots(committee): - m[(applicant, committee, interval)] = model.add_var( - var_type=mip.BINARY, name=f"({applicant}, {committee}, {interval})") - - # Legger inn begrensninger for at en komité kun kan ha antall møter i et slot lik kapasiteten. - for committee in committees: - for interval, capacity in committee.get_intervals_and_capacities(): - model += mip.xsum(m[(applicant, committee, interval)] - for applicant in committee.get_applicants() - if (applicant, committee, interval) in m) <= capacity # type: ignore - - # Legger inn begrensninger for at en person kun har ett intervju med hver komité - for applicant in applicants: - for committee in applicant.get_committees(): - model += mip.xsum(m[(applicant, committee, interval)] - for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # type: ignore - - # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt - for applicant in applicants: - for interval in applicant.get_intervals(): - model += mip.xsum(m[(applicant, committee, interval)] - for committee in applicant.get_committees() - if (applicant, committee, interval) in m) <= 1 # type: ignore +def print_matchings(committees: list[Committee], intervals: list[TimeInterval], matchings: list[tuple[Applicant, Committee, TimeInterval]]): - # Setter mål til å være maksimering av antall møter - model.objective = mip.maximize(mip.xsum(m.values())) + print("Tid".ljust(15), end="|") + print("|".join(str(com).ljust(8) for com in committees)) - # Kjør optimeringen - solver_status = model.optimize() + for interval in intervals: + print(interval.start.strftime("%d.%m %H:%M").ljust(15), end="|") + print() - # Få de faktiske møtetidene - antall_matchede_møter: int = 0 - matchings: list = [] - for name, variable in m.items(): - if variable.x: - antall_matchede_møter += 1 - matchings.append(name) - print(f"{name}") + # for komite in solution: + # print(komite.ljust(8), end="|") + # print("|".join([str(slot).rjust(2) for slot in solution[komite]])) - antall_ønskede_møter = sum( - len(applicant.get_committees()) for applicant in applicants) - - match_object: MeetingMatch = { - "solver_status": solver_status, - "matched_meetings": antall_matchede_møter, - "total_wanted_meetings": antall_ønskede_møter, - "matchings": matchings, - } - - return match_object + # print("|".join(["Slot"] + [komite.rjust(8) for komite in komiteer])) + # for slot in solution2: + # print(str(slot).ljust(4), end="|") + # print("|".join([str(solution2[slot][komite]).rjust(8) + # for komite in solution2[slot]])) class MipTest(unittest.TestCase): + def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInterval]]): """Checks if the constraints are satisfied in the provided matchings. TODO: Add more constraint tests.""" + print("Matchings:") + for matching in matchings: + print(matching) + self.assertEqual(len(matchings), len(set((applicant, interval) for applicant, _, interval in matchings)), "Constraint \"Applicant can only have one meeting during each TimeInterval\" failed.") @@ -107,33 +66,43 @@ def test_fixed_small(self): """Small, fixed test with all capacities set to one""" appkom = Committee(name="Appkom") - appkom.add_intervals_with_capacities({TimeInterval(1, 5): 1}) + appkom.add_intervals_with_capacities( + {TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 15)): 1}) oil = Committee(name="OIL") - oil.add_intervals_with_capacities({TimeInterval(4, 6): 1}) + oil.add_intervals_with_capacities( + {TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1}) prokom = Committee(name="Prokom") - prokom.add_intervals_with_capacities({TimeInterval(1, 3): 1, - TimeInterval(4, 6): 1}) + prokom.add_intervals_with_capacities({TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 45)): 1, + TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1}) committees: set[Committee] = {appkom, oil, prokom} jørgen: Applicant = Applicant(name="Jørgen") jørgen.add_committees({appkom, prokom}) - jørgen.add_intervals({TimeInterval(1, 4)}) + jørgen.add_intervals( + {TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 0))}) sindre: Applicant = Applicant(name="Sindre") sindre.add_committees({appkom, oil}) - sindre.add_intervals({TimeInterval(2, 3), TimeInterval(4, 6)}) + sindre.add_intervals({TimeInterval(datetime(2024, 8, 24, 8, 30), datetime( + 2024, 8, 24, 8, 45)), + TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30))}) julian: Applicant = Applicant(name="Julian") julian.add_committees({appkom, prokom, oil}) julian.add_intervals( - {TimeInterval(3, 4), TimeInterval(1, 2), TimeInterval(5, 6)}) + {TimeInterval(datetime(2024, 8, 24, 8, 45), datetime(2024, 8, 24, 9, 0)), + TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 9, 15), datetime(2024, 8, 24, 9, 30))}) fritz: Applicant = Applicant(name="Fritz") fritz.add_committees({oil}) - fritz.add_intervals({TimeInterval(1, 2), TimeInterval(4, 5)}) + fritz.add_intervals( + {TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)), + TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 15))}) applicants: set[Applicant] = {jørgen, sindre, julian, fritz} @@ -151,28 +120,45 @@ def test_fixed_small(self): self.check_constraints(matchings=match["matchings"]) def test_randomized_large(self): + self.randomized_test(200, (10, 20), (5*5*2, 8*5*2), (1, 3)) + self.randomized_test(200, (10, 20), (5*5*2, 8*5*2), (3, 3)) + self.randomized_test(350, (15, 25), (5*5*2, 8*5*2), (3, 3)) + + def randomized_test(self, + antall_personer: int, + antall_slots_per_person_interval: tuple[int, int], + antall_slots_per_komite_interval: tuple[int, int], + antall_komiteer_per_person_interval: tuple[int, int]): """ - Tests a randomized selection of applicants, committees and slots. - All committees have a capacity of one. + Tester tilfeldige utvalg av søkere, komitéer og tidsintervaller. + Alle komitéer har en kapasitet lik 1. - This test is without asserts, and mostly to test performance. + Tester først og fremst performance. + TODO: Legg til flere asserts. """ import random - START_TID = 0 - # Hadde -1 her, men husker ikke hvorfor, så har fjernet det inntil videre. - SLUTT_TID = 10*5*2 - ANTALL_PERSONER = 400 + DEFAULT_INTERVIEW_TIME = timedelta(minutes=15) - ANTALL_SLOTS_PER_PERSON_MIN = 10 - ANTALL_SLOTS_PER_PERSON_MAKS = 20 + SLOTS: list[TimeInterval] = [] + for dag in range(0, 5): + """Lager slots for fra 0800 til 1800 hver dag""" + dagsintervall = TimeInterval( + datetime(2024, 8, 19+dag, 8, 0), datetime(2024, 8, 19+dag, 18, 0)) + [SLOTS.append(interval) for interval in dagsintervall.divide( + DEFAULT_INTERVIEW_TIME)] - ANTALL_SLOTS_PER_KOMITE_MIN = 5*5*2 - ANTALL_SLOTS_PER_KOMITE_MAKS = 8*5*2 + ANTALL_PERSONER = antall_personer - ANTALL_KOMITEER_PER_PERSON_MIN = 1 - ANTALL_KOMITEER_PER_PERSON_MAKS = 3 + ANTALL_SLOTS_PER_PERSON_MIN = antall_slots_per_person_interval[0] + ANTALL_SLOTS_PER_PERSON_MAKS = antall_slots_per_person_interval[1] + + ANTALL_SLOTS_PER_KOMITE_MIN = antall_slots_per_komite_interval[0] + ANTALL_SLOTS_PER_KOMITE_MAKS = antall_slots_per_komite_interval[1] + + ANTALL_KOMITEER_PER_PERSON_MIN = antall_komiteer_per_person_interval[0] + ANTALL_KOMITEER_PER_PERSON_MAKS = antall_komiteer_per_person_interval[1] komite_navn = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} @@ -184,22 +170,22 @@ def test_randomized_large(self): for person in range(ANTALL_PERSONER): applicant = Applicant(name=str(person)) # Velger ut et tilfeldig antall slots (alle av lengde 1) innenfor boundsene. - applicant.add_intervals(set(TimeInterval(start_tid, start_tid+1) for start_tid in set((random.sample(range( - START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))))) + applicant.add_intervals(set(random.sample(SLOTS, random.randint( + ANTALL_SLOTS_PER_PERSON_MIN, ANTALL_SLOTS_PER_PERSON_MAKS)))) applicants.add(applicant) # Gir intervaller til hver komité. for committee in committees: - committee.add_intervals_with_capacities({TimeInterval(start_tid, start_tid + 1): 1 for start_tid in (random.sample(range( - START_TID, SLUTT_TID+1), random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS)))}) + committee.add_intervals_with_capacities({slot: 1 for slot in random.sample( + SLOTS, random.randint(ANTALL_SLOTS_PER_KOMITE_MIN, ANTALL_SLOTS_PER_KOMITE_MAKS))}) # Lar hver søker søke på tilfeldige komiteer committees_list = list(committees) # Må ha liste for at random.sample skal kunne velge ut riktig for applicant in applicants: applicant.add_committees(set(random.sample(committees_list, random.randint( - ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) # type: ignore + ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) match = match_meetings(applicants=applicants, committees=committees) self.check_constraints(matchings=match["matchings"]) @@ -207,3 +193,5 @@ def test_randomized_large(self): print( f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") print(f"Solver status: {match['solver_status']}") + + print_matchings(committees_list, SLOTS, match["matchings"]) From cfe3617d358d36a53b3f4cc01e3eb49641b201d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 18 Apr 2024 15:42:33 +0200 Subject: [PATCH 15/75] feat: add support for union of timeinterval --- algorithm/mip_matching/TimeInterval.py | 25 +++++++++++++--- .../mip_matching/tests/TimeIntervalTest.py | 29 +++++++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py index 0c8296ca..c6196a28 100644 --- a/algorithm/mip_matching/TimeInterval.py +++ b/algorithm/mip_matching/TimeInterval.py @@ -14,21 +14,38 @@ class TimeInterval: end: datetime def __post_init__(self) -> None: - """Metode som sikrer at start og end er av type datetime.""" + """Metode som sikrer at start og end er av type datetime og at de er i kronologisk rekkefølge.""" if not (isinstance(self.start, datetime) and isinstance(self.end, datetime)): - raise ValueError("Start and end must be of type datetime.") + raise TypeError("Start and end must be of type datetime.") - def overlaps(self, other: TimeInterval) -> bool: + if not (self.start <= self.end): + raise ValueError("Start must be before end") + + def intersects(self, other: TimeInterval) -> bool: """Returnerer true om to tidsintervaller er helt eller delvis overlappende.""" return other.start <= self.start < other.end or self.start <= other.start < self.end + def is_tangent_to(self, other: TimeInterval) -> bool: + """Returnerer true om self tangerer other (er helt inntil, men ikke overlappende).""" + return not self.intersects(other) and (other.start == self.end or self.start == other.end) + + def union(self, other: TimeInterval) -> TimeInterval: + """Returnerer union av tidsintervall dersom de to intervallene har overlapp eller er inntil hverandre""" + + if not (self.is_tangent_to(other) or self.intersects(other)): + raise ValueError("Cannot have union with gaps between") + + start = min(self.start, other.start) + end = max(self.end, other.end) + return TimeInterval(start, end) + def contains(self, other: TimeInterval) -> bool: """Returnerer true om other inngår helt i self.""" return self.start <= other.start and other.end <= self.end def intersection(self, other: TimeInterval) -> TimeInterval | None: """Returnerer et snitt av to tidsintervaller.""" - if not self.overlaps(other): + if not self.intersects(other): # Snittet er tomt grunnet ingen overlapp return None diff --git a/algorithm/mip_matching/tests/TimeIntervalTest.py b/algorithm/mip_matching/tests/TimeIntervalTest.py index fa68d29d..41f6edef 100644 --- a/algorithm/mip_matching/tests/TimeIntervalTest.py +++ b/algorithm/mip_matching/tests/TimeIntervalTest.py @@ -11,6 +11,14 @@ class TimeIntervalTest(unittest.TestCase): def setUp(self): self.interval = TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 30)) + self.t1: TimeInterval = TimeInterval( + datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 45)) + self.t2: TimeInterval = TimeInterval( + datetime(2024, 8, 24, 8, 45), datetime(2024, 8, 24, 9, 0)) + self.t3: TimeInterval = TimeInterval( + datetime(2024, 8, 24, 8, 30), datetime(2024, 8, 24, 9, 0)) + self.t4: TimeInterval = TimeInterval( + datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)) def test_overlapping(self): @@ -20,7 +28,7 @@ def test_overlapping(self): datetime(2024, 8, 24, 8, 15), datetime(2024, 8, 24, 8, 45)) - self.assertTrue(interval1.overlaps(interval2)) + self.assertTrue(interval1.intersects(interval2)) def test_overlapping_edge(self): interval1: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), @@ -28,12 +36,12 @@ def test_overlapping_edge(self): interval2: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 15), datetime(2024, 8, 24, 8, 30)) - self.assertFalse(interval1.overlaps(interval2)) + self.assertFalse(interval1.intersects(interval2)) interval3: TimeInterval = TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)) - self.assertTrue(interval1.overlaps(interval3)) + self.assertTrue(interval1.intersects(interval3)) def test_division(self): actual_division = self.interval.divide(timedelta(minutes=30)) @@ -77,6 +85,21 @@ def test_intersection(self): datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 9, 15)), self.interval.intersection(TimeInterval(datetime(2024, 8, 24, 6, 45), datetime(2024, 8, 24, 9, 15)))) + def test_tangent(self): + self.assertTrue(self.t1.is_tangent_to(self.t2)) + self.assertFalse(self.t1.is_tangent_to(self.t3)) + self.assertFalse(self.t4.is_tangent_to(self.t2)) + + def test_union(self): + with self.assertRaises(ValueError): + self.t4.union(self.t2) + + self.assertEqual(self.t1.union(self.t2), TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 9, 0))) + + self.assertEqual(self.t3.union(self.t4), TimeInterval(datetime(2024, 8, 24, 8, 0), + datetime(2024, 8, 24, 9, 0))) + def test_get_contained_slots(self): test_case_slots = [TimeInterval(datetime(2024, 8, 24, 7, 45), datetime(2024, 8, 24, 8, 30)), From c361957a0a89beba60eade68afb483a89d435c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 25 Apr 2024 15:05:52 +0200 Subject: [PATCH 16/75] feat: La til sanitizing i Applicant --- algorithm/mip_matching/Applicant.py | 23 ++++++++++++++ algorithm/mip_matching/TimeInterval.py | 5 ++- algorithm/mip_matching/tests/ApplicantTest.py | 31 +++++++++++++++---- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/algorithm/mip_matching/Applicant.py b/algorithm/mip_matching/Applicant.py index a09abf39..9c72ac2d 100644 --- a/algorithm/mip_matching/Applicant.py +++ b/algorithm/mip_matching/Applicant.py @@ -30,6 +30,29 @@ def add_committees(self, committees: set[Committee]) -> None: def add_interval(self, interval: TimeInterval) -> None: # TODO: Vurder å gjøre "sanitizing" ved å slå sammen overlappende intervaller. + """ + Slår også sammen overlappende intervaller. + + Maksimalt to typer slots som må merges: + - Alle som inngår i dette intervallet + - De to som grenser møtes i grensene. + Merger først med førstnevnte, fordi etter det vil det kun være (opptil) to som kan merges (i sistnevnte kategori) + """ + for other in interval.get_contained_slots(list(self.slots)): + self.slots.remove(other) + interval = interval.union(other) + + slots_to_merge = set() + for _ in range(2): + for other in self.slots: + if interval.is_mergable(other): + # Må legge til en liste midlertidig for å unngå concurrency errors. + slots_to_merge.add(other) + + for slot in slots_to_merge: + self.slots.remove(slot) + interval = interval.union(slot) + self.slots.add(interval) def add_intervals(self, intervals: set[TimeInterval]) -> None: diff --git a/algorithm/mip_matching/TimeInterval.py b/algorithm/mip_matching/TimeInterval.py index c6196a28..f2156d5e 100644 --- a/algorithm/mip_matching/TimeInterval.py +++ b/algorithm/mip_matching/TimeInterval.py @@ -32,7 +32,7 @@ def is_tangent_to(self, other: TimeInterval) -> bool: def union(self, other: TimeInterval) -> TimeInterval: """Returnerer union av tidsintervall dersom de to intervallene har overlapp eller er inntil hverandre""" - if not (self.is_tangent_to(other) or self.intersects(other)): + if not self.is_mergable(other): raise ValueError("Cannot have union with gaps between") start = min(self.start, other.start) @@ -43,6 +43,9 @@ def contains(self, other: TimeInterval) -> bool: """Returnerer true om other inngår helt i self.""" return self.start <= other.start and other.end <= self.end + def is_mergable(self, other: TimeInterval) -> bool: + return self.intersects(other) or self.is_tangent_to(other) + def intersection(self, other: TimeInterval) -> TimeInterval | None: """Returnerer et snitt av to tidsintervaller.""" if not self.intersects(other): diff --git a/algorithm/mip_matching/tests/ApplicantTest.py b/algorithm/mip_matching/tests/ApplicantTest.py index cf66737c..a52d517b 100644 --- a/algorithm/mip_matching/tests/ApplicantTest.py +++ b/algorithm/mip_matching/tests/ApplicantTest.py @@ -15,13 +15,14 @@ def setUp(self) -> None: TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30)): 1 }) - def test_get_fitting_committee_slots(self) -> None: - test_applicant = Applicant("Test Testesen") + self.test_applicant = Applicant("Test Testesen") + + self.test_applicant.add_interval(TimeInterval(datetime(2024, 8, 24, 7, 30), + datetime(2024, 8, 24, 10, 0))) - test_applicant.add_interval(TimeInterval(datetime(2024, 8, 24, 7, 30), - datetime(2024, 8, 24, 10, 0))) + def test_get_fitting_committee_slots(self) -> None: - test_applicant.get_fitting_committee_slots(self.committee) + self.test_applicant.get_fitting_committee_slots(self.committee) self.assertEqual(set([TimeInterval(datetime(2024, 8, 24, 8, 0), datetime(2024, 8, 24, 8, 30)), @@ -29,4 +30,22 @@ def test_get_fitting_committee_slots(self) -> None: datetime(2024, 8, 24, 9, 0)), TimeInterval(datetime(2024, 8, 24, 9, 0), datetime(2024, 8, 24, 9, 30))]), - test_applicant.get_fitting_committee_slots(self.committee)) + self.test_applicant.get_fitting_committee_slots(self.committee)) + + def test_add_interval_sanitizes(self) -> None: + + self.test_applicant.add_intervals({ + TimeInterval(datetime(2024, 8, 24, 9, 30), + datetime(2024, 8, 24, 10, 30)), + TimeInterval(datetime(2024, 8, 24, 4, 30), + datetime(2024, 8, 24, 6, 0)), + TimeInterval(datetime(2024, 8, 24, 10, 30), + datetime(2024, 8, 24, 11, 30)) + }) + + self.assertEqual(self.test_applicant.get_intervals(), { + TimeInterval(datetime(2024, 8, 24, 4, 30), + datetime(2024, 8, 24, 6, 0)), + TimeInterval(datetime(2024, 8, 24, 7, 30), + datetime(2024, 8, 24, 11, 30)), + }) From 2b1ca8ac05e75c2aef083808da61c0ac33907dd5 Mon Sep 17 00:00:00 2001 From: sindreeh Date: Thu, 25 Apr 2024 16:00:08 +0200 Subject: [PATCH 17/75] Simple skeleton for aws ses params --- package-lock.json | 2139 +++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + utils/sendEmail.ts | 54 ++ 3 files changed, 2178 insertions(+), 16 deletions(-) create mode 100644 utils/sendEmail.ts diff --git a/package-lock.json b/package-lock.json index 8f10d877..1a773667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "online-opptak", "version": "0.1.0", "dependencies": { + "@aws-sdk/client-ses": "^3.556.0", "@prisma/client": "^4.16.2", "@reduxjs/toolkit": "^1.9.7", "@tailwindcss/forms": "^0.5.7", @@ -53,6 +54,590 @@ "typescript": "4.8.3" } }, + "node_modules/@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "dependencies": { + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-sdk/client-ses": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.556.0.tgz", + "integrity": "sha512-Bs3MW317rhqpEFPaWGmgqwB3olkNFKsfWCVI4RP3AzhlxcxOLpqHigVcdJ880jqfWv2twsnWBhJFyjEl/EC7jw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/credential-provider-node": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "@smithy/util-waiter": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.556.0.tgz", + "integrity": "sha512-unXdWS7uvHqCcOyC1de+Fr8m3F2vMg2m24GPea0bg7rVGTYmiyn9mhUX11VCt+ozydrw+F50FQwL6OqoqPocmw==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.556.0.tgz", + "integrity": "sha512-AXKd2TB6nNrksu+OfmHl8uI07PdgzOo4o8AxoRO8SHlwoMAGvcT9optDGVSYoVfgOKTymCoE7h8/UoUfPc11wQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.556.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.556.0.tgz", + "integrity": "sha512-TsK3js7Suh9xEmC886aY+bv0KdLLYtzrcmVt6sJ/W6EnDXYQhBuKYFhp03NrN2+vSvMGpqJwR62DyfKe1G0QzQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.556.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.556.0.tgz", + "integrity": "sha512-vJaSaHw2kPQlo11j/Rzuz0gk1tEaKdz+2ser0f0qZ5vwFlANjt08m/frU17ctnVKC1s58bxpctO/1P894fHLrA==", + "dependencies": { + "@smithy/core": "^1.4.2", + "@smithy/protocol-http": "^3.3.0", + "@smithy/signature-v4": "^2.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.535.0.tgz", + "integrity": "sha512-XppwO8c0GCGSAvdzyJOhbtktSEaShg14VJKg8mpMa1XcgqzmcqqHQjtDWbx5rZheY1VdpXZhpEzJkB6LpQejpA==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.552.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.552.0.tgz", + "integrity": "sha512-vsmu7Cz1i45pFEqzVb4JcFmAmVnWFNLsGheZc8SCptlqCO5voETrZZILHYIl4cjKkSDk3pblBOf0PhyjqWW6WQ==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.556.0.tgz", + "integrity": "sha512-0Nz4ErOlXhe3muxWYMbPwRMgfKmVbBp36BAE2uv/z5wTbfdBkcgUwaflEvlKCLUTdHzuZsQk+BFS/gVyaUeOuA==", + "dependencies": { + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.556.0", + "@aws-sdk/credential-provider-web-identity": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.556.0.tgz", + "integrity": "sha512-s1xVtKjyGc60O8qcNIzS1X3H+pWEwEfZ7TgNznVDNyuXvLrlNWiAcigPWGl2aAkc8tGcsSG0Qpyw2KYC939LFg==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-http": "3.552.0", + "@aws-sdk/credential-provider-ini": "3.556.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.556.0", + "@aws-sdk/credential-provider-web-identity": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.535.0.tgz", + "integrity": "sha512-9O1OaprGCnlb/kYl8RwmH7Mlg8JREZctB8r9sa1KhSsWFq/SWO0AuJTyowxD7zL5PkeS4eTvzFFHWCa3OO5epA==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.556.0.tgz", + "integrity": "sha512-ETuBgcnpfxqadEAqhQFWpKoV1C/NAgvs5CbBc5EJbelJ8f4prTdErIHjrRtVT8c02MXj92QwczsiNYd5IoOqyw==", + "dependencies": { + "@aws-sdk/client-sso": "3.556.0", + "@aws-sdk/token-providers": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.556.0.tgz", + "integrity": "sha512-R/YAL8Uh8i+dzVjzMnbcWLIGeeRi2mioHVGnVF+minmaIkCiQMZg2HPrdlKm49El+RljT28Nl5YHRuiqzEIwMA==", + "dependencies": { + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.535.0.tgz", + "integrity": "sha512-0h6TWjBWtDaYwHMQJI9ulafeS4lLaw1vIxRjbpH0svFRt6Eve+Sy8NlVhECfTU2hNz/fLubvrUxsXoThaLBIew==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.535.0.tgz", + "integrity": "sha512-huNHpONOrEDrdRTvSQr1cJiRMNf0S52NDXtaPzdxiubTkP+vni2MohmZANMOai/qT0olmEVX01LhZ0ZAOgmg6A==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.535.0.tgz", + "integrity": "sha512-am2qgGs+gwqmR4wHLWpzlZ8PWhm4ktj5bYSgDrsOfjhdBlWNxvPoID9/pDAz5RWL48+oH7I6SQzMqxXsFDikrw==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.540.0.tgz", + "integrity": "sha512-8Rd6wPeXDnOYzWj1XCmOKcx/Q87L0K1/EHqOBocGjLVbN3gmRxBvpmR1pRTjf7IsWfnnzN5btqtcAkfDPYQUMQ==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.535.0.tgz", + "integrity": "sha512-IXOznDiaItBjsQy4Fil0kzX/J3HxIOknEphqHbOfUf+LpA5ugcsxuQQONrbEQusCBnfJyymrldBvBhFmtlU9Wg==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.556.0.tgz", + "integrity": "sha512-tvIiugNF0/+2wfuImMrpKjXMx4nCnFWQjQvouObny+wrif/PGqqQYrybwxPJDvzbd965bu1I+QuSv85/ug7xsg==", + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.540.0.tgz", + "integrity": "sha512-1kMyQFAWx6f8alaI6UT65/5YW/7pDWAKAdNwL6vuJLea03KrZRX3PMoONOSJpAS5m3Ot7HlWZvf3wZDNTLELZw==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "@smithy/util-endpoints": "^1.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.535.0.tgz", + "integrity": "sha512-PHJ3SL6d2jpcgbqdgiPxkXpu7Drc2PYViwxSIqvvMKhDwzSB1W3mMvtpzwKM4IE7zLFodZo0GKjJ9AsoXndXhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.535.0.tgz", + "integrity": "sha512-RWMcF/xV5n+nhaA/Ff5P3yNP3Kur/I+VNZngog4TEs92oB/nwOdAg/2JL8bVAhUbMrjTjpwm7PItziYFQoqyig==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.535.0.tgz", + "integrity": "sha512-dRek0zUuIT25wOWJlsRm97nTkUlh1NDcLsQZIN2Y8KxhwoXXWtJs5vaDPT+qAg+OpcNj80i1zLR/CirqlFg/TQ==", + "dependencies": { + "@aws-sdk/types": "3.535.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, "node_modules/@babel/runtime": { "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", @@ -499,6 +1084,542 @@ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", "dev": true }, + "node_modules/@smithy/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz", + "integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==", + "dependencies": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.2.tgz", + "integrity": "sha512-2fek3I0KZHWJlRLvRTqxTEri+qV0GRHrJIoLFuBMZB4EMg4WgeBGfF0X6abnrNYpq55KJ6R4D6x4f0vLnhzinA==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz", + "integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==", + "dependencies": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz", + "integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==", + "dependencies": { + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-node": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz", + "integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==", + "dependencies": { + "@smithy/types": "^2.12.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz", + "integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz", + "integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==", + "dependencies": { + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.1.tgz", + "integrity": "sha512-1/8kFp6Fl4OsSIVTWHnNjLnTL8IqpIb/D3sTSczrKFnrE9VMNWxnrRKNvpUHOJ6zpGD5f62TPm7+17ilTJpiCQ==", + "dependencies": { + "@smithy/middleware-serde": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.1.tgz", + "integrity": "sha512-P2bGufFpFdYcWvqpyqqmalRtwFUNUA8vHjJR5iGqbfR6mp65qKOLcUd6lTr4S9Gn/enynSrSf3p3FVgVAf6bXA==", + "dependencies": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/service-error-classification": "^2.1.5", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz", + "integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz", + "integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz", + "integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==", + "dependencies": { + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz", + "integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==", + "dependencies": { + "@smithy/abort-controller": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz", + "integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz", + "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz", + "integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==", + "dependencies": { + "@smithy/types": "^2.12.0", + "@smithy/util-uri-escape": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz", + "integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", + "dependencies": { + "@smithy/types": "^2.12.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz", + "integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz", + "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-uri-escape": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.1.tgz", + "integrity": "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ==", + "dependencies": { + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz", + "integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==", + "dependencies": { + "@smithy/querystring-parser": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz", + "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz", + "integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==", + "dependencies": { + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz", + "integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==", + "dependencies": { + "@smithy/config-resolver": "^2.2.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz", + "integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==", + "dependencies": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz", + "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==", + "dependencies": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz", + "integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==", + "dependencies": { + "@smithy/service-error-classification": "^2.1.5", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz", + "integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==", + "dependencies": { + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz", + "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.2.0.tgz", + "integrity": "sha512-IHk53BVw6MPMi2Gsn+hCng8rFA3ZmR3Rk7GllxDUW9qFJl/hiSvskn7XldkECapQVkIg/1dHpMAxI9xSTaLLSA==", + "dependencies": { + "@smithy/abort-controller": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", @@ -1177,6 +2298,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2340,16 +3466,37 @@ "node": ">= 6" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" - }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + }, + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -5660,6 +6807,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -5824,9 +6976,9 @@ } }, "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -6266,6 +7418,520 @@ } }, "dependencies": { + "@aws-crypto/ie11-detection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", + "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha256-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", + "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", + "requires": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/sha256-js": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/sha256-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", + "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", + "requires": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/supports-web-crypto": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", + "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", + "requires": { + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-crypto/util": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", + "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", + "requires": { + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "@aws-sdk/client-ses": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.556.0.tgz", + "integrity": "sha512-Bs3MW317rhqpEFPaWGmgqwB3olkNFKsfWCVI4RP3AzhlxcxOLpqHigVcdJ880jqfWv2twsnWBhJFyjEl/EC7jw==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/credential-provider-node": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "@smithy/util-waiter": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/client-sso": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.556.0.tgz", + "integrity": "sha512-unXdWS7uvHqCcOyC1de+Fr8m3F2vMg2m24GPea0bg7rVGTYmiyn9mhUX11VCt+ozydrw+F50FQwL6OqoqPocmw==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/client-sso-oidc": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.556.0.tgz", + "integrity": "sha512-AXKd2TB6nNrksu+OfmHl8uI07PdgzOo4o8AxoRO8SHlwoMAGvcT9optDGVSYoVfgOKTymCoE7h8/UoUfPc11wQ==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/client-sts": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.556.0.tgz", + "integrity": "sha512-TsK3js7Suh9xEmC886aY+bv0KdLLYtzrcmVt6sJ/W6EnDXYQhBuKYFhp03NrN2+vSvMGpqJwR62DyfKe1G0QzQ==", + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.556.0", + "@aws-sdk/middleware-host-header": "3.535.0", + "@aws-sdk/middleware-logger": "3.535.0", + "@aws-sdk/middleware-recursion-detection": "3.535.0", + "@aws-sdk/middleware-user-agent": "3.540.0", + "@aws-sdk/region-config-resolver": "3.535.0", + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@aws-sdk/util-user-agent-browser": "3.535.0", + "@aws-sdk/util-user-agent-node": "3.535.0", + "@smithy/config-resolver": "^2.2.0", + "@smithy/core": "^1.4.2", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/hash-node": "^2.2.0", + "@smithy/invalid-dependency": "^2.2.0", + "@smithy/middleware-content-length": "^2.2.0", + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-body-length-browser": "^2.2.0", + "@smithy/util-body-length-node": "^2.3.0", + "@smithy/util-defaults-mode-browser": "^2.2.1", + "@smithy/util-defaults-mode-node": "^2.3.1", + "@smithy/util-endpoints": "^1.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/core": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.556.0.tgz", + "integrity": "sha512-vJaSaHw2kPQlo11j/Rzuz0gk1tEaKdz+2ser0f0qZ5vwFlANjt08m/frU17ctnVKC1s58bxpctO/1P894fHLrA==", + "requires": { + "@smithy/core": "^1.4.2", + "@smithy/protocol-http": "^3.3.0", + "@smithy/signature-v4": "^2.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.535.0.tgz", + "integrity": "sha512-XppwO8c0GCGSAvdzyJOhbtktSEaShg14VJKg8mpMa1XcgqzmcqqHQjtDWbx5rZheY1VdpXZhpEzJkB6LpQejpA==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-http": { + "version": "3.552.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.552.0.tgz", + "integrity": "sha512-vsmu7Cz1i45pFEqzVb4JcFmAmVnWFNLsGheZc8SCptlqCO5voETrZZILHYIl4cjKkSDk3pblBOf0PhyjqWW6WQ==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.556.0.tgz", + "integrity": "sha512-0Nz4ErOlXhe3muxWYMbPwRMgfKmVbBp36BAE2uv/z5wTbfdBkcgUwaflEvlKCLUTdHzuZsQk+BFS/gVyaUeOuA==", + "requires": { + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.556.0", + "@aws-sdk/credential-provider-web-identity": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.556.0.tgz", + "integrity": "sha512-s1xVtKjyGc60O8qcNIzS1X3H+pWEwEfZ7TgNznVDNyuXvLrlNWiAcigPWGl2aAkc8tGcsSG0Qpyw2KYC939LFg==", + "requires": { + "@aws-sdk/credential-provider-env": "3.535.0", + "@aws-sdk/credential-provider-http": "3.552.0", + "@aws-sdk/credential-provider-ini": "3.556.0", + "@aws-sdk/credential-provider-process": "3.535.0", + "@aws-sdk/credential-provider-sso": "3.556.0", + "@aws-sdk/credential-provider-web-identity": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.535.0.tgz", + "integrity": "sha512-9O1OaprGCnlb/kYl8RwmH7Mlg8JREZctB8r9sa1KhSsWFq/SWO0AuJTyowxD7zL5PkeS4eTvzFFHWCa3OO5epA==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.556.0.tgz", + "integrity": "sha512-ETuBgcnpfxqadEAqhQFWpKoV1C/NAgvs5CbBc5EJbelJ8f4prTdErIHjrRtVT8c02MXj92QwczsiNYd5IoOqyw==", + "requires": { + "@aws-sdk/client-sso": "3.556.0", + "@aws-sdk/token-providers": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.556.0.tgz", + "integrity": "sha512-R/YAL8Uh8i+dzVjzMnbcWLIGeeRi2mioHVGnVF+minmaIkCiQMZg2HPrdlKm49El+RljT28Nl5YHRuiqzEIwMA==", + "requires": { + "@aws-sdk/client-sts": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.535.0.tgz", + "integrity": "sha512-0h6TWjBWtDaYwHMQJI9ulafeS4lLaw1vIxRjbpH0svFRt6Eve+Sy8NlVhECfTU2hNz/fLubvrUxsXoThaLBIew==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.535.0.tgz", + "integrity": "sha512-huNHpONOrEDrdRTvSQr1cJiRMNf0S52NDXtaPzdxiubTkP+vni2MohmZANMOai/qT0olmEVX01LhZ0ZAOgmg6A==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-recursion-detection": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.535.0.tgz", + "integrity": "sha512-am2qgGs+gwqmR4wHLWpzlZ8PWhm4ktj5bYSgDrsOfjhdBlWNxvPoID9/pDAz5RWL48+oH7I6SQzMqxXsFDikrw==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.540.0.tgz", + "integrity": "sha512-8Rd6wPeXDnOYzWj1XCmOKcx/Q87L0K1/EHqOBocGjLVbN3gmRxBvpmR1pRTjf7IsWfnnzN5btqtcAkfDPYQUMQ==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@aws-sdk/util-endpoints": "3.540.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/region-config-resolver": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.535.0.tgz", + "integrity": "sha512-IXOznDiaItBjsQy4Fil0kzX/J3HxIOknEphqHbOfUf+LpA5ugcsxuQQONrbEQusCBnfJyymrldBvBhFmtlU9Wg==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/token-providers": { + "version": "3.556.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.556.0.tgz", + "integrity": "sha512-tvIiugNF0/+2wfuImMrpKjXMx4nCnFWQjQvouObny+wrif/PGqqQYrybwxPJDvzbd965bu1I+QuSv85/ug7xsg==", + "requires": { + "@aws-sdk/client-sso-oidc": "3.556.0", + "@aws-sdk/types": "3.535.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/types": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.535.0.tgz", + "integrity": "sha512-aY4MYfduNj+sRR37U7XxYR8wemfbKP6lx00ze2M2uubn7mZotuVrWYAafbMSXrdEMSToE5JDhr28vArSOoLcSg==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.540.0.tgz", + "integrity": "sha512-1kMyQFAWx6f8alaI6UT65/5YW/7pDWAKAdNwL6vuJLea03KrZRX3PMoONOSJpAS5m3Ot7HlWZvf3wZDNTLELZw==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "@smithy/util-endpoints": "^1.2.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-locate-window": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.535.0.tgz", + "integrity": "sha512-PHJ3SL6d2jpcgbqdgiPxkXpu7Drc2PYViwxSIqvvMKhDwzSB1W3mMvtpzwKM4IE7zLFodZo0GKjJ9AsoXndXhA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.535.0.tgz", + "integrity": "sha512-RWMcF/xV5n+nhaA/Ff5P3yNP3Kur/I+VNZngog4TEs92oB/nwOdAg/2JL8bVAhUbMrjTjpwm7PItziYFQoqyig==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/types": "^2.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.535.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.535.0.tgz", + "integrity": "sha512-dRek0zUuIT25wOWJlsRm97nTkUlh1NDcLsQZIN2Y8KxhwoXXWtJs5vaDPT+qAg+OpcNj80i1zLR/CirqlFg/TQ==", + "requires": { + "@aws-sdk/types": "3.535.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "requires": { + "tslib": "^2.3.1" + } + }, "@babel/runtime": { "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", @@ -6522,6 +8188,429 @@ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", "dev": true }, + "@smithy/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/config-resolver": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz", + "integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==", + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/core": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.2.tgz", + "integrity": "sha512-2fek3I0KZHWJlRLvRTqxTEri+qV0GRHrJIoLFuBMZB4EMg4WgeBGfF0X6abnrNYpq55KJ6R4D6x4f0vLnhzinA==", + "requires": { + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/credential-provider-imds": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz", + "integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==", + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/fetch-http-handler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz", + "integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==", + "requires": { + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/hash-node": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz", + "integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==", + "requires": { + "@smithy/types": "^2.12.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/invalid-dependency": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz", + "integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-content-length": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz", + "integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==", + "requires": { + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-endpoint": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.1.tgz", + "integrity": "sha512-1/8kFp6Fl4OsSIVTWHnNjLnTL8IqpIb/D3sTSczrKFnrE9VMNWxnrRKNvpUHOJ6zpGD5f62TPm7+17ilTJpiCQ==", + "requires": { + "@smithy/middleware-serde": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-retry": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.1.tgz", + "integrity": "sha512-P2bGufFpFdYcWvqpyqqmalRtwFUNUA8vHjJR5iGqbfR6mp65qKOLcUd6lTr4S9Gn/enynSrSf3p3FVgVAf6bXA==", + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/service-error-classification": "^2.1.5", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "dependencies": { + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } + }, + "@smithy/middleware-serde": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz", + "integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz", + "integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/node-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz", + "integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==", + "requires": { + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/node-http-handler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz", + "integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==", + "requires": { + "@smithy/abort-controller": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/property-provider": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz", + "integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/protocol-http": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz", + "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/querystring-builder": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz", + "integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==", + "requires": { + "@smithy/types": "^2.12.0", + "@smithy/util-uri-escape": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/querystring-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz", + "integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/service-error-classification": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", + "requires": { + "@smithy/types": "^2.12.0" + } + }, + "@smithy/shared-ini-file-loader": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz", + "integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/signature-v4": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz", + "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==", + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-uri-escape": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/smithy-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.1.tgz", + "integrity": "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ==", + "requires": { + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/url-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz", + "integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==", + "requires": { + "@smithy/querystring-parser": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-browser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-node": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-config-provider": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz", + "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-defaults-mode-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz", + "integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==", + "requires": { + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-defaults-mode-node": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz", + "integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==", + "requires": { + "@smithy/config-resolver": "^2.2.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-endpoints": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz", + "integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==", + "requires": { + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-hex-encoding": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-middleware": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz", + "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==", + "requires": { + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-retry": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz", + "integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==", + "requires": { + "@smithy/service-error-classification": "^2.1.5", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz", + "integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==", + "requires": { + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-uri-escape": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz", + "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-waiter": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.2.0.tgz", + "integrity": "sha512-IHk53BVw6MPMi2Gsn+hCng8rFA3ZmR3Rk7GllxDUW9qFJl/hiSvskn7XldkECapQVkIg/1dHpMAxI9xSTaLLSA==", + "requires": { + "@smithy/abort-controller": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + } + }, "@swc/helpers": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", @@ -7024,6 +9113,11 @@ } } }, + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7887,6 +9981,14 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "fast-xml-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", + "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "requires": { + "strnum": "^1.0.5" + } + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -10030,6 +12132,11 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -10145,9 +12252,9 @@ } }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 1772ab37..b22b5cdd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "format:write": "prettier --write ." }, "dependencies": { + "@aws-sdk/client-ses": "^3.556.0", "@prisma/client": "^4.16.2", "@reduxjs/toolkit": "^1.9.7", "@tailwindcss/forms": "^0.5.7", diff --git a/utils/sendEmail.ts b/utils/sendEmail.ts new file mode 100644 index 00000000..28143adf --- /dev/null +++ b/utils/sendEmail.ts @@ -0,0 +1,54 @@ +import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses'; + +export default async function sendEmail() { + + const sesClient = new SESClient({}); + + const params = { + Source: "STRING_VALUE", + Destination: { + ToAddresses: [ + "STRING_VALUE", + ], + CcAddresses: [ + "STRING_VALUE", + ], + BccAddresses: [ + "STRING_VALUE", + ], + }, + Message: { + Subject: { + Data: "STRING_VALUE", + Charset: "STRING_VALUE", + }, + Body: { + Text: { + Data: "STRING_VALUE", + Charset: "STRING_VALUE", + }, + Html: { + Data: "STRING_VALUE", + Charset: "STRING_VALUE", + }, + }, + }, + ReplyToAddresses: [ + "STRING_VALUE", + ], + ReturnPath: "STRING_VALUE", + SourceArn: "STRING_VALUE", + ReturnPathArn: "STRING_VALUE", + Tags: [ + { + Name: "STRING_VALUE", + Value: "STRING_VALUE", + }, + ], + ConfigurationSetName: "STRING_VALUE", + }; + + const command = new SendEmailCommand(params); + const response = await sesClient.send(command); + +} \ No newline at end of file From 2f4305965fa7519f378db67d655a3a43649899e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 28 Apr 2024 14:30:10 +0200 Subject: [PATCH 18/75] test: la til test med sammenhengende intervaller og ulike kapasiteter --- algorithm/mip_matching/tests/mip_test.py | 134 ++++++++++++++++++++--- algorithm/requirements.txt | Bin 90 -> 208 bytes 2 files changed, 116 insertions(+), 18 deletions(-) diff --git a/algorithm/mip_matching/tests/mip_test.py b/algorithm/mip_matching/tests/mip_test.py index 1eb8cd3a..56de36fc 100644 --- a/algorithm/mip_matching/tests/mip_test.py +++ b/algorithm/mip_matching/tests/mip_test.py @@ -1,5 +1,5 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date, time from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee @@ -10,27 +10,30 @@ from typing import TypedDict +from faker import Faker + import unittest +import random -def print_matchings(committees: list[Committee], intervals: list[TimeInterval], matchings: list[tuple[Applicant, Committee, TimeInterval]]): +def print_matchings(committees: list[Committee], + intervals: list[TimeInterval], + matchings: list[tuple[Applicant, Committee, TimeInterval]]): print("Tid".ljust(15), end="|") print("|".join(str(com).ljust(8) for com in committees)) for interval in intervals: print(interval.start.strftime("%d.%m %H:%M").ljust(15), end="|") - print() + for committee in committees: + name = "" + cands = [a.name for a, c, + i in matchings if interval == i and c == committee] + name = cands[0] if len(cands) > 0 else "" - # for komite in solution: - # print(komite.ljust(8), end="|") - # print("|".join([str(slot).rjust(2) for slot in solution[komite]])) + print(name.rjust(8), end="|") - # print("|".join(["Slot"] + [komite.rjust(8) for komite in komiteer])) - # for slot in solution2: - # print(str(slot).ljust(4), end="|") - # print("|".join([str(solution2[slot][komite]).rjust(8) - # for komite in solution2[slot]])) + print() class MipTest(unittest.TestCase): @@ -39,9 +42,9 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte """Checks if the constraints are satisfied in the provided matchings. TODO: Add more constraint tests.""" - print("Matchings:") - for matching in matchings: - print(matching) + # print("Matchings:") + # for matching in matchings: + # print(matching) self.assertEqual(len(matchings), len(set((applicant, interval) for applicant, _, interval in matchings)), @@ -62,6 +65,10 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte self.assertGreaterEqual(committee.get_capacity(interval), load, f"Constraint \"Number of interviews per slot per committee cannot exceed capacity\" failed for Committee {committee} and interval {interval}") + def get_default_slots(self): + """ + Returnerer slots""" + def test_fixed_small(self): """Small, fixed test with all capacities set to one""" @@ -124,6 +131,99 @@ def test_randomized_large(self): self.randomized_test(200, (10, 20), (5*5*2, 8*5*2), (3, 3)) self.randomized_test(350, (15, 25), (5*5*2, 8*5*2), (3, 3)) + def test_randomized_with_different_interview_sizes(self): + """ + Plan: + Lager flere komitéer med ulike intervjulengder + """ + pass + + def test_randomized_with_continuous_intervals(self): + """ + Gjør en randomisert test hvor hver person kan i sammenhengende + tidsperioder i stedet for tilfeldige slots. + + Hver komité har fremdeles like lange intervjutider. + """ + fake = Faker() + + ANTALL_PERSONER = 400 + + DEFAULT_INTERVIEW_TIME = timedelta(minutes=20) + + # ANTALL_INTERVALL_PER_PERSON_MIN = 5 + # ANTALL_INTERVALL_PER_PERSON_MAKS = 8 + # INTERVALLENGDE_PER_PERSON_MIN = timedelta(minutes=30) + INTERVALLENGDE_PER_PERSON_MAKS = timedelta(hours=10) + ANTALL_INTERVALL_FORSØK = 4 + + START_DATE = date(2024, 8, 26) + END_DATE = date(2024, 8, 30) + START_TIME_PER_DAY = time(hour=8, minute=0) + END_TIME_PER_DAY = time(hour=18, minute=0) + + def get_random_interval(interval_date: date) -> TimeInterval: + interval_start = datetime.combine(interval_date, START_TIME_PER_DAY) + \ + fake.time_delta(INTERVALLENGDE_PER_PERSON_MAKS) + + interval_end = interval_start + \ + fake.time_delta(INTERVALLENGDE_PER_PERSON_MAKS) + + if interval_end > datetime.combine(interval_date, END_TIME_PER_DAY): + interval_end = datetime.combine( + interval_date, END_TIME_PER_DAY) + + return TimeInterval(interval_start, interval_end) + + # Gir tider til hver søker + applicants: set[Applicant] = set() + for person in range(ANTALL_PERSONER): + applicant = Applicant(name=str(person)) + + for _ in range(ANTALL_INTERVALL_FORSØK): + interval_date = fake.date_between_dates(START_DATE, END_DATE) + + applicant.add_interval( + get_random_interval(interval_date=interval_date)) + + applicants.add(applicant) + + KAPASITET_PER_INTERVALL_MIN = 1 + KAPASITET_PER_INTERVALL_MAKS = 3 + # INTERVALLENGDE_PER_KOMTIE_MAKS = timedelta(hours=10) + ANTALL_INTERVALL_FORSØK_KOMITE = 10 + + ANTALL_KOMITEER_PER_PERSON_MIN = 2 + ANTALL_KOMITEER_PER_PERSON_MAKS = 3 + + # print([applicant.get_intervals() for applicant in applicants]) + + # Gir intervaller til hver komité. + committees: set[Committee] = { + Committee(name=navn, interview_length=DEFAULT_INTERVIEW_TIME) for navn in {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", + "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"}} + for committee in committees: + + for _ in range(ANTALL_INTERVALL_FORSØK_KOMITE): + interval_date = fake.date_between_dates(START_DATE, END_DATE) + + committee.add_intervals_with_capacities({get_random_interval(interval_date): random.randint( + KAPASITET_PER_INTERVALL_MIN, KAPASITET_PER_INTERVALL_MAKS)}) + + # Lar hver søker søke på tilfeldige komiteer + committees_list = list(committees) + # Må ha liste for at random.sample skal kunne velge ut riktig + for applicant in applicants: + applicant.add_committees(set(random.sample(committees_list, random.randint( + ANTALL_KOMITEER_PER_PERSON_MIN, ANTALL_KOMITEER_PER_PERSON_MAKS)))) + + match = match_meetings(applicants=applicants, committees=committees) + self.check_constraints(matchings=match["matchings"]) + + print( + f"Klarte å matche {match['matched_meetings']} av {match['total_wanted_meetings']} ({match['matched_meetings']/match['total_wanted_meetings']:2f})") + print(f"Solver status: {match['solver_status']}") + def randomized_test(self, antall_personer: int, antall_slots_per_person_interval: tuple[int, int], @@ -137,9 +237,7 @@ def randomized_test(self, TODO: Legg til flere asserts. """ - import random - - DEFAULT_INTERVIEW_TIME = timedelta(minutes=15) + DEFAULT_INTERVIEW_TIME = timedelta(minutes=20) SLOTS: list[TimeInterval] = [] for dag in range(0, 5): @@ -163,7 +261,7 @@ def randomized_test(self, komite_navn = {"Appkom", "Prokom", "Arrkom", "Dotkom", "Bankkom", "OIL", "Fagkom", "Bedkom", "FemInIT", "Backlog", "Trikom"} committees: set[Committee] = { - Committee(name=navn) for navn in komite_navn} + Committee(name=navn, interview_length=DEFAULT_INTERVIEW_TIME) for navn in komite_navn} # Gir tider til hver søker applicants: set[Applicant] = set() diff --git a/algorithm/requirements.txt b/algorithm/requirements.txt index a910af925a406ea85a4f357cf4d7689b597af9b5..349630b1bd346bcbe0b649e972a8500e82667c86 100644 GIT binary patch delta 123 zcma#Lz&Jrp+KnNRA)6tUp@_j22#pv_81xtnfl!aZV4|dBSOG&NLkU9$Lq07&Sp|qMZ!@4qgJ~ From b79a2afd371e8eabe094cf21d18cf58fee98502a Mon Sep 17 00:00:00 2001 From: sindreeh Date: Sun, 5 May 2024 12:46:41 +0200 Subject: [PATCH 19/75] SES setup and test page --- pages/emailTest.tsx | 25 +++++++++++++++++++++++++ pages/index.tsx | 6 ++++-- utils/sendEmail.ts | 43 +++++++++++-------------------------------- 3 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 pages/emailTest.tsx diff --git a/pages/emailTest.tsx b/pages/emailTest.tsx new file mode 100644 index 00000000..c8365063 --- /dev/null +++ b/pages/emailTest.tsx @@ -0,0 +1,25 @@ +import Button from "../components/Button"; +import sendEmail from "../utils/sendEmail"; +import { useState, useEffect } from "react"; + +export default function EmailTest() { + const [timesRun, setTimesRun] = useState(0); + + useEffect(() => { + console.log(timesRun); + }, [timesRun]); + + return ( +
+
+ ); +} \ No newline at end of file diff --git a/pages/index.tsx b/pages/index.tsx index e9fb8d8b..3dceecd8 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,6 +6,7 @@ import AuthenticationIllustration from "../components/icons/illustrations/Authen import SelectionIllustration from "../components/icons/illustrations/SelectionIllustration"; import CustomizationIllustration from "../components/icons/illustrations/CustomizationIllustration"; import Button from "../components/Button"; +import EmailTest from "./emailTest"; const Home = () => { const { data: session } = useSession(); @@ -17,7 +18,7 @@ const Home = () => { return (
- + {/* {!session && (
@@ -51,7 +52,8 @@ const Home = () => {
)} -