diff --git a/api/spec/packages/aip-client-javascript/src/funcs/apps.ts b/api/spec/packages/aip-client-javascript/src/funcs/apps.ts index c164e34247..40e81fa321 100644 --- a/api/spec/packages/aip-client-javascript/src/funcs/apps.ts +++ b/api/spec/packages/aip-client-javascript/src/funcs/apps.ts @@ -16,6 +16,8 @@ export function listApps( ): Promise> { const searchParams = toURLSearchParams({ page: req.page, + sort: encodeSort(req.sort), + filter: req.filter, }) return request(() => http(client) diff --git a/api/spec/packages/aip-client-javascript/src/index.ts b/api/spec/packages/aip-client-javascript/src/index.ts index d8a05931a8..01142cef30 100644 --- a/api/spec/packages/aip-client-javascript/src/index.ts +++ b/api/spec/packages/aip-client-javascript/src/index.ts @@ -194,6 +194,7 @@ export type { LlmCostOverrideCreate, ListCustomersParamsFilter, ListSubscriptionsParamsFilter, + ListAppsParamsFilter, ListFeatureParamsFilter, ListAddonsParamsFilter, CreateCreditGrantTaxConfig, diff --git a/api/spec/packages/aip-client-javascript/src/models/operations/apps.ts b/api/spec/packages/aip-client-javascript/src/models/operations/apps.ts index 96fc3606b5..542c08f59a 100644 --- a/api/spec/packages/aip-client-javascript/src/models/operations/apps.ts +++ b/api/spec/packages/aip-client-javascript/src/models/operations/apps.ts @@ -1,10 +1,18 @@ import { z } from 'zod' import * as schemas from '../schemas.js' -import type { AppPagePaginatedResponse } from '../types.js' +import type { + AppPagePaginatedResponse, + ListAppsParamsFilter, + SortQueryInput, +} from '../types.js' export interface ListAppsQuery { /** Determines which page of the collection to retrieve. */ page?: { size?: number; number?: number } + /** Sort apps returned in the response. Supported sort attributes are: - `id` - `created_at` (default) The `asc` suffix is optional as the default sort order is ascending. The `desc` suffix is used to specify a descending order. */ + sort?: SortQueryInput + /** Filter apps returned in the response. To filter apps by name add the following query param: filter[name]=my-app */ + filter?: ListAppsParamsFilter } export type ListAppsRequest = ListAppsQuery diff --git a/api/spec/packages/aip-client-javascript/src/models/schemas.ts b/api/spec/packages/aip-client-javascript/src/models/schemas.ts index 532f636365..fc083d6ddf 100644 --- a/api/spec/packages/aip-client-javascript/src/models/schemas.ts +++ b/api/spec/packages/aip-client-javascript/src/models/schemas.ts @@ -2904,6 +2904,15 @@ export const listSubscriptionsParamsFilter = z }) .describe('Filter options for listing subscriptions.') +export const listAppsParamsFilter = z + .object({ + id: ulidFieldFilter.optional(), + name: stringFieldFilter.optional(), + type: stringFieldFilterExact.optional(), + status: stringFieldFilterExact.optional(), + }) + .describe('Filter options for listing apps.') + export const listFeatureParamsFilter = z .object({ meter_id: ulidFieldFilter.optional(), @@ -5083,6 +5092,8 @@ export const listAppsQueryParams = z.object({ }) .optional() .describe('Determines which page of the collection to retrieve.'), + sort: sortQuery.optional(), + filter: listAppsParamsFilter.optional(), }) export const listAppsResponse = z.object({ diff --git a/api/spec/packages/aip-client-javascript/src/models/types.ts b/api/spec/packages/aip-client-javascript/src/models/types.ts index e8423e8ba0..81e27ab7a6 100644 --- a/api/spec/packages/aip-client-javascript/src/models/types.ts +++ b/api/spec/packages/aip-client-javascript/src/models/types.ts @@ -1745,6 +1745,27 @@ export interface ListSubscriptionsParamsFilter { plan_key?: string | { eq?: string; oeq?: string[]; neq?: string } } +/** Filter options for listing apps. */ +export interface ListAppsParamsFilter { + id?: string | { eq?: string; oeq?: string[]; neq?: string } + name?: + | string + | { + eq?: string + neq?: string + contains?: string + ocontains?: string[] + oeq?: string[] + gt?: string + gte?: string + lt?: string + lte?: string + exists?: boolean + } + type?: string | { eq?: string; oeq?: string[]; neq?: string } + status?: string | { eq?: string; oeq?: string[]; neq?: string } +} + /** Filter options for listing features. */ export interface ListFeatureParamsFilter { meter_id?: string | { eq?: string; oeq?: string[]; neq?: string } diff --git a/api/spec/packages/aip/src/apps/operations.tsp b/api/spec/packages/aip/src/apps/operations.tsp index cf6ab395f9..b45b383a2b 100644 --- a/api/spec/packages/aip/src/apps/operations.tsp +++ b/api/spec/packages/aip/src/apps/operations.tsp @@ -15,6 +15,21 @@ using TypeSpec.OpenAPI; namespace Apps; +/** + * Filter options for listing apps. + */ +@friendlyName("ListAppsParamsFilter") +model ListAppsParamsFilter { + #suppress "@openmeter/api-spec-aip/doc-decorator" "filter field" + id?: Common.ULIDFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "filter field" + name?: Common.StringFieldFilter; + #suppress "@openmeter/api-spec-aip/doc-decorator" "filter field" + type?: Common.StringFieldFilterExact; + #suppress "@openmeter/api-spec-aip/doc-decorator" "filter field" + status?: Common.StringFieldFilterExact; +} + interface AppsOperations { /** * List installed apps. @@ -22,9 +37,29 @@ interface AppsOperations { @get @operationId("list-apps") @summary("List apps") - list(...Common.PagePaginationQuery): - | Shared.PagePaginatedResponse - | Common.ErrorResponses; + list( + ...Common.PagePaginationQuery, + + /** + * Sort apps returned in the response. Supported sort attributes are: + * + * - `id` + * - `created_at` (default) + * + * The `asc` suffix is optional as the default sort order is ascending. The `desc` + * suffix is used to specify a descending order. + */ + @query(#{ name: "sort" }) + sort?: Common.SortQuery, + + /** + * Filter apps returned in the response. + * + * To filter apps by name add the following query param: filter[name]=my-app + */ + @query(#{ style: "deepObject", explode: true }) + filter?: ListAppsParamsFilter, + ): Shared.PagePaginatedResponse | Common.ErrorResponses; /** * Get an installed app. diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 4cd4a73818..5111ff7ba7 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -5046,6 +5046,25 @@ type ListAddonsParamsFilter struct { Status *StringFieldFilterExact `json:"status,omitempty"` } +// ListAppsParamsFilter Filter options for listing apps. +type ListAppsParamsFilter struct { + // Id Filters on the given ULID field value by exact match. All properties are + // optional; provide exactly one to specify the comparison. + Id *ULIDFieldFilter `json:"id,omitempty"` + + // Name Filters on the given string field value by either exact or fuzzy match. All + // properties are optional; provide exactly one to specify the comparison. + Name *StringFieldFilter `json:"name,omitempty"` + + // Status Filters on the given string field value by exact match. All properties are + // optional; provide exactly one to specify the comparison. + Status *StringFieldFilterExact `json:"status,omitempty"` + + // Type Filters on the given string field value by exact match. All properties are + // optional; provide exactly one to specify the comparison. + Type *StringFieldFilterExact `json:"type,omitempty"` +} + // ListChargesParamsFilter Filter options for listing charges. type ListChargesParamsFilter struct { // Status Filter charges by status. @@ -6038,6 +6057,20 @@ type ListAddonsParams struct { type ListAppsParams struct { // Page Determines which page of the collection to retrieve. Page *PagePaginationQuery `json:"page,omitempty"` + + // Sort Sort apps returned in the response. Supported sort attributes are: + // + // - `id` + // - `created_at` (default) + // + // The `asc` suffix is optional as the default sort order is ascending. The `desc` + // suffix is used to specify a descending order. + Sort *SortQuery `form:"sort,omitempty" json:"sort,omitempty"` + + // Filter Filter apps returned in the response. + // + // To filter apps by name add the following query param: filter[name]=my-app + Filter *ListAppsParamsFilter `json:"filter,omitempty"` } // ListCurrenciesParams defines parameters for ListCurrencies. @@ -8570,6 +8603,22 @@ func (siw *ServerInterfaceWrapper) ListApps(w http.ResponseWriter, r *http.Reque return } + // ------------- Optional query parameter "sort" ------------- + + err = runtime.BindQueryParameterWithOptions("form", false, false, "sort", r.URL.Query(), ¶ms.Sort, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sort", Err: err}) + return + } + + // ------------- Optional query parameter "filter" ------------- + + err = filters.Parse(r.URL.Query(), ¶ms.Filter) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "filter", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.ListApps(w, r, params) })) @@ -11550,261 +11599,263 @@ var swaggerSpec = []string{ "3tv5Z0vKwbbBXu5o3yxUTW3dnLs+vzuXRiQu8a1XjyGqEE9/CSEtbeG76zN9d8L2avjYr4s8R7912yGO", "Zz6V6gOaMeo4mjAB0BfwEHWz5AVAuYPlCRu2gbayD/3eT9ZL/guks9+jv/NTccgyTCDrd9ObiHsHLCUX", "hqzo2jsQg/FQn0PuwPirooBXU/zrDTz+stuMOVwNpo40z50LR2/WH/sfIJhnSO9LRHqL7OWc6+zkpp8u", - "uY3vqqB1Mix6HvbdJQMvX/T/Y+97nNu2kf3/FXw1b+aSd5JsJ2mvzZubjus0be6axq9OrnNXeRSYhCS8", - "UABLkI7VTP7372AXAEESlChZshzbM53GNvFjAewugMXuZ3N9Ba3s1t5W2OW24B3xNn51bLOLfg4kMPPU", - "rHIxOTyzGhatosaVMTU7acYqVkL4Y+YisLgg763tS396D0ZBdgknf/z7SOBBY/n+1DBH4iMm+UiVQx1d", - "MqNuDw6wplmo8I3Qbr7+KoZvhP4xB3bN8gyrj1lgfs3At5nxpk3YHba0ToaoJdV+QjFTZk62BdhWc4i9", - "pwkHFzleP44Z6qw9UtG5b5CWSUJThVY7C/hqhQvI7iM74MJ5y1tek/VEsZjMGN7l2rfhVv3SlJeAht2y", - "F99b57ln36ul0pMVAarBjIpm2r6/KF8e4GE0YREy+0jAyuPMLePHdzgMAACqqVTLjp6twtMClRkIKdo6", - "HmsztunV2RsCbou2TBnlFMtIDWM+5fDaYp1Vlf57MbdejQe6gC558O3h4eHRs2++GRw9PXDvwwfZmCs5", - "1j2Mbeb/McZYDGf5/LEFEq/65Z8e/duPfjKj6FUiqk8fffd8NIr/Cv8M9U+Pv/v34+8Cf30d/Otvwb++", - "gL++DXz5aY22zx5/9/i7//JvlPVZDtl7XhkY9B8uGeBR3EpwBXDJ4GJaIXYPyAqvxCVNeHxqI0JPZpJ3", - "dGmobf9QUfkTUIcobcitP1SLH2SKG9d6iO6pvmMa792Wxioq0TzClHUhBsZOD4YOdWqqSAJmOI5TV0bT", - "KoS48Yxu8O95hx5QJ1SJvZDxYqVh0RuDAnEAWvtuObqs+QuW6uunyDdY9tjWra983Qv0/i2+m5ox9AVr", - "0XFWtskYyA+OP7wF68IbJrzGhkisxRvXWD+Dsl+p/E0IjT600ACc796M5xj2TgX5xgsF2enKz+nVOMEZ", - "w5QCKBfw837UgZ3PTkuOWQFufMltMoKNlxxdBCze9s2tNhfeanMx1ke53KRIGSfyI8siqpj5vUjTyu+Y", - "mt+WdozCxZ4YxaxCF0Y5y6mIaRbfHI/sQK0vTcOAA/4VFnyLs91lclUXXvTvhFKwLpkl2tbwc3+9ilUt", - "sXbtyraybm3vkLpu1epZB5zcqjbr1ceSCldsojAcZ/R7XI1tV1wB1ObXz8zP5o0MftGX07HBreBqbJUh", - "/KKPFPYnQN6Bnw1jwc+GKeHnouCm38kfsbAUgKcHPhCX6bT0zID5t4T0xPIQvdqzNh41ztiUXSHCph66", - "6dTCf40Fyz/K7MPYf50c/ykFGydc5W2lIx5n44tERh/qJWxEtu7XuwJucrr6+efXJ1IBFGyy7gPiz68J", - "1gt7bldLly95pU0kZkmfsOF02Cej3jTNB89GPf1jlNAiZoOng68GSgrB8lEvGEgUDpD9xQuMrfXx4+lb", - "28cJ9EGeDr8iZ+19tAZPhhSYP5enGEe2Lhqq/ODFoIH9WUC8qhlHzIScm2t4Cg/hH5gIGKJpNGNjvfbj", - "lGVjKLUVsE/dLtHtGlBlRwN59O7sxWNwnsbOP2Y8Z7voHRpe0j0XaZFvt+NXusklXcoi33qfb6DNJZ3i", - "Rmow/rfX76+2WSKXU1CXjNq0B2Zlici8uWRZxmOGWRPWFBpcHi0siAgBMQYpywZaUFVKIwcBbnoZEgOr", - "nGJSF9ClfZSxkYCsnzYagLyaaBG0NQlNNPcvCLrck2pMaK0pQkVc4kf2Cc/JR54kkGAUja2N5p2vL7Rf", - "qWJitbcRLbEilvSkgngJD6gWbHd7WUJ/w4cXrsrB2zR3rrda31sKegz0zK5SnjEF/cHSBYG7T6iQArKO", - "4vKWLwrBzQkbCm9RNb87bE8XDbaUentJp9GHNqLAI0Vls0GgS0yQBFwceC83Xw4umYhldR9fvXe6dr05", - "LsdWCeCs8dsSxXHqsgWs8d4rSggIVAwZi2QW90uYBfvij2Wc7jMPwhY41ygMeLfz/nxjeDknFgDHAUUM", - "twMkvo6CII/KuBOafKQLRUa9d2cvRr3HQWp2qktwOQOKZCkhW1csSIbVKhDPBYHA9rEZv8Nbrp265bRu", - "IX7WbHlVpdXoaO7uAWsrmrbQbqsdmij0N6fWgr2Xam6t7q0WDDnXmE8NzdjovbSjrNk3j9gZ1g0yoMM3", - "BQ6L9E1Ii1mQhi2/ef/sQdEsU0ihK1V9b2jbGMy0NbRIO5TNqr3jzC1Ey3VVabHNmL2IebtGZXZLw7co", - "qKYfUeKCfuV1Rlr/2n1aztcmN+/yoGovxjJlgnK8GVORzzKZ8mjjW3ez/TcpE8evsP3jpe2vd+N2KGJt", - "Pkwe2NXXT+tYVz6sOh38eTj4FsDVjz4/Kn8dDMfn/+19/at5j1+OToKEEZXLjDlHO4DBERa6yU86Utgc", - "yeh+BFcYVxB9hTJMbstoFs3ge5RJpVxji1TvNCMxEv9kC1VmTp4QNMyTo8HXT733AIzjNN7UCEQCrjgj", - "CMLFhfoghWBRjr/MmZqZP+uVA4erUW886g1Hwne5+NRj4rL3vJczZdygKggSX3kIEmb1QuvKVQ6ZxdV2", - "Y9D1VMLkYdLo5ZepTQLO+x1RCyph4p3w8wLh5aUorl2xDKjeZJSfW5YMkxzuaM0wOVJgzcqx7AgsADsm", - "FwvfVfHMJXewoeSEZuy5/jQg78129B5+MbmJ4ecJFzTBHw065Xtdpe5f77fpHOjBwTZFU4erPVyyHFLl", - "31O1swXRW+GFbj+EaUPz8XrBeisuHXYxXKcAm8C1Dq3ZK1qno8x7s6sZwTghzGd0A5aaKpyESUhVB7NY", - "VzyaWYLOsIXu/RsxWbUUXkad3S5I7nW0jWVpS7S1bHZ8GvYFOFKnYRnqiNZCWG0kKvNH1sQcCTbiA3Rs", - "wJYe50AuvnUHrrtexp64NnxnmtO1H0JAjDfb0u10rsG4OHfts4B+wj+U+B428h4dQF8Yd891gw6dg7iH", - "HGKdn11ISTjxTjsUqPWoD7dZJPnaWbtD424LQFoW0NWKtelP8s44rTWc2KWIzuSEJzZX5W0/uKYJFeNN", - "e00zPqfZYszmBpho7RYQDMKLnRib2IkNaWqTPXCG3hFPcONwTRh00h7HNF7X7NhpL8JebcZbDyfRqYVX", - "L4brggausReW/dt+zHxsZAjbYMgAswnYCvrPEGpkSRj2NrANbjR2gxeOB0SZ7XX0SADSgtK068FjL3gK", - "MZl6djpwHKvuaoOTz0ZDXHHCcfiWu1AwNqooGIq9kdoFN7LNdqgNd5q2ifNtxzvSz1Uzs9oJPmI7C3lX", - "E5Pykg2nQ/IdWiZ/t9/Of2d/nP/93dmL+pv5LgjC9/FXL2q02G6RFvAfaz68744eeK8PUaQ/nP8eSZFT", - "LhRQ1tvoxWk9qty7c5Um+2ecJTT073ibKfeXKin4RyTEvI+0ChqEh+1IwtArtVU9bX0+jBfsxQJu2r5W", - "2mFX6E7SOr+nCd2V1UWf0XdiW78jZvKz4sLN5I6WQPldrDzhr7mlblQJLm6b19xk6R3fbH8ZX7P1l+vY", - "oJZDuEYlxQ2+BcZsAplaZ/IjySUCPcArnYPf925s3nub/Yy9qKL2HA55IZ+15IWsUfiKoPfEOwPpVs3U", - "YJxDev81tI/0Zl7+awg/fO73gMCx+XOayXkK7u0xpEj78euv/vO3r746fvnb8T9/+uHoyS//Pjz5329f", - "/mTyzTzvgZeVGgPYsX32NQ4dirw1f/XdFpYNDSCSS19+oBJaMnuxD3Hoz2BXpQwscOzVDPu/eG3DoVwv", - "baHK9AYGav9z/yGb2pefTW0rmU1+oXMWk3+cvfnllOYzwq70jJgsJ5Kwq1yThG7HmSxSvdcDr3sIInjf", - "BBNqwyXAJECE538pFJhNBTjvpTMq0DEckcxEzDIVyYzV5sHTPQ0F0NCUvkYI2W3N1dhIhoX1M1gtnvBU", - "cR2calmRvAwV5niraVvAw0lxYSDKUKmrGUAgWfqNoibvFJsUkCFUfeApkYmzupFXk5GoJYmjSUJmXOUy", - "Azdjc5unGbPtxsMvKWffLclfd9sTtDW3qvqUBXRBXRWYnDlWAVRtvH9RoAtcPrsFzPNbv1yZc0cV8z6h", - "l9M+mXOBbkJzeuWLosIjjM2FngG4j5dP1Tg1pTRTFp4Ry0K3L2VmdNAY8Nb8lvtV4pEoq7ioSdo6JC8B", - "/LUQ+UhUtlc7D24qNaF8KtC86OsPdxZYwT4bJ8/rVw4VFTV43naePK4eQzY+S5TeiOYwqKfKhXKO7a/0", - "cmrirDEsXws4eG2FfBaBwNubDgfP43tJhgNde0mY1Ea3uGpWJptFqg2RsdMx46aTRiGxIJxL8q2NBJw2", - "TA72R6Me+wPdCrkY9R576adx43NBr23ZqKrL8XnpAv2YUVEkNOMhJfsWDlSuQCUpGhy0QPVAEmyLP2VI", - "tBu4PrVYfCbIFzinuS+Np2+PXvf6+h995Tk9egH/f90ub9eAnT72OchHrvQOb3gq0neop/YOdfgc/tP3", - "p6k/WYZaPRkQuNx73nv39gSTrHktPPFa+Lwsz1n3G1ZFstqzpNfZ00jOTlP2YWY6UiYs4col3kS/8M+1", - "aVx/5D7LtpDE/2SBDH4wJXhB8AgUuWzJTjwSbgy1dH94oxA5z5iftBTaHl8sxlVttDxLo0+SccsgF4sK", - "X/7eq1gXztfA3q0rf49dPWhxw7lNQl2+Q02dfzyFadXtEKqMmcZhS786/uUYtcJ/dIEXJqfwSAD63fOD", - "g48fPw45FXQos+mBbmmgW1KPEXa1bNrLQh/r5Z9zgRcM4DmMSw0na1RtCaffvT2BctC+iz1VLWk5d5NU", - "cpmQ5HK4SmVvhOj6uqL6LGiip/nwbPF7fSetmEWXG6ysLQxenQZ5kV3IngfVkxZeutA2DduuO82VoPe8", - "d/Rk+PTZV1/DPG/a2ufuXlS4RIgKDDyF2ZW1UvAzQBE2T/MFgqgjgreB+O7qYuUt8I5zL2+mtfcnC508", - "x6rTt6X8zLUjpy8uX4yUbHZSfkg1fKtTDZtV3k6qYa8DTKLYkDrsbq00whbLFDFMN9isuJgaS+hEJon8", - "aAPQTxJZIDKqcgHmTXNoqdErAifx4jhP9aHnJ5Yksk8+yiyJ/x8MC+wflYOTk0iQ7K+io8MJjdngKPqW", - "DZ7FX0eDb5787atB9NWT6OnXf3t6FD+NypjM5z2TgWFg7COa3EuWKRzl0fCw57l3OSUyAJMKOmFVNEDt", - "Naf6pNS6o3VNT1VanlO6SCSNh8S+EPQJnxBjzSM898xP/zh78wuRxnWsNct7yRWaKMh4JfKw/fsEP6It", - "x0iGv+Kw9yKXkjf61lyKyqhnkh8CdPL/KSlGPcLVSFDNPvbk/tPbt6f+DbReRzNzaRRrfO2QyV6TiIK3", - "NIYWzrFQzLx16pHReMYy/RFQ6x1Ic5HxhlluJR1LA19V+ShSNQN2ZPEVFma1OoYYwSQg+Zneej/OOLzt", - "Gh6c0TRlom6jrMmTPz8DH49sFXW+HPrXIBTJwDUIC4cYsqKCzCjK96bChIKVQ8AuVhFY+nzWXgrgtwvL", - "PiY1kE1MBl2aY4yd2so3k0stG4lHDvUgLn2THldJrSqkFSRv5jS6Cije4UPIyPjKsJI1yGuth1Bk9B3x", - "15cn5OnTp99WR7FEg64UoXYdRblQxGgi84B6YXcoq7twzjMGWUitFUZmHNONiOlIlKOqzbycD81vQyXn", - "DFraxDDv0AF8ljc1SzY7r2WF1wP5wXTZurFXQcrXxptxLxrz6mYPIdf4sZKRpbq5+3lFVp257ZunK3sj", - "u/gYnzw22MYrzvfLanpe6oFiT7DY5+unZLEBOS65/aqkLLyij5Qf0QDL6+er9VdnDUugO1mGSTFSljid", - "6AUTXCu4YdkDOOTiao9kgO88V34OJbiEWEf/zYMONqAKe7O7RWxsdPZywWPMJtGWJ8rYBk0xmy7K7DXV", - "l9b1DCB1xfIv14nLFbU0ssxqSn+R/ak99zMRGjLXU3V1itbTfNBEc+p2kGO2bOD6uWUDGEaYHJY4wDYA", - "vuqaIfUEM592z8qqu5wVcyrIbcnM+ovMX8pCxCUTdIvZpYpZTv7USJuQmyC88iTwi9T330LEqLlUTkUd", - "bPqDFNPneUYj9vzoydNnX339t2++Paz6eLrCzw6ffXbTUe/npe3HHnvKzzZtDfxrgBggc82zw2che/W5", - "niFjdGji4hiPKuchovS5n2YXPM9ottA3zYjDedv4SFST04xGg+9+Pxx8e/7XR6PREH9qQaB54yW2MnlJ", - "39IrzX1r54j0Whok7JIlxFwbSE6vkPvdDcTAYWilgwf1elGFefrxVOhQ8kz4qT26WyxNfYX1M3SZpJF6", - "gvSmUuRyTnMeQT7r8rzsp/TiaglW5nZdLCund+tAGcbgwwzgAJEwzunVmtgYZh2XnUle1BfIAKJWASmc", - "TvI+uCoodZcSgLdukErXZzV1rvljhbxd5unyXLU6wJY156ltldcDJzulU/aahd5n3E0sLXM44huXh7Ru", - "U6SCD7Vz0ZoYs1X1paYqGwY73deE5bXLAatfDVTK6AdG1WKQsyyjE5nNB+hjVaLb8T+rKtXz1FivJXQF", - "rza1WVu19XNVgVbbUcuCeN5BjVXBCUXN5y0MN/4feAWtznRqdv0VPkks6I6U2j3ayEmVvDr9Haf5NKEC", - "8LbWdeay9erbm7kD6Zm4MO6cFOKCMKBBGASuPqj/TMJWMBLGLIbgXOBbU2Zv1IoiLbJoRhWDixW3TQaO", - "k24onRQEjMBpLZMWIfh2AJQ1bniw9IYQd9SEog9u/XfCrX+SyfkYwpBSzX7d56niQB1kqQ/MeZqBeEAH", - "6MdbmlcN31lc27BQ+Nz3Utcv29ue0/i6LuBzejX+o6Cw1m13K1yYcqsCrvHHbf2JzUBj5+8KQkdeyswm", - "Fx3YS4NTIoCGCslnSkxEk9cVPBXnRZLzRjWtipgoIdkKAYmMWUzsYBpEDb3UQX7q79f0ylXqhZI5PXjJ", - "d/eSX2WpsUndgyaHTnaZU3g0yE9oThM5DRhk2m7b/6p3uSrPezdfctzKmiooeFKxu/Ht9dMuDxp78dXW", - "3d/eyTG4U5rIPU1PxiN2e+engvm/pwlaph3Wm6OGwjAADoBaDsYxFoeONVypgu3AhKryrIjyImOxtcls", - "25T6Gs2oZd4LGLeB7Vzffuqy6DUPFSnVx3J8g4Ri1RdHUKrqIM3kQUZzFtEsVgfgEHNgsGv+Ce9ZrWnf", - "TTK77ibdWsKPGzTn2nkKsXMjCmRtV0K0P5ik+SY9BkaCOX4ckjcpy2iuOVxf6eZFXoD5jl1FSaH4JetD", - "AOpIQI56UxZe0owrC80JNeBJDa4XoUQtcn4BkfQeFnlsiFT2US6RUwiyPP7lRefDQXO+aj7oy1LpgVig", - "BaclusvOGLHlqgMw7mqhKNc/VrVowm06tsfFqva4IrBak0CTanmi/prP/tIJE11nzCVOMhW6jlSsnroy", - "J5Oew84Nd5pD3exNzKPMriklb369ISEpH2xQOxFsgxg4kC5KzIayXV+XCaJHTMDSZNTanKarVdtI1HQb", - "eVBtt0S1IW7xyjahlNeAzUX6oBwflOPtU46vaUp0nSVa8lcWFZkufAoxKGsqR1fbhrDgDAhCRTQDTQlG", - "fS5yll3SJKTMdLntmJbAQjQALx/TfS4hJYAxktVIrUOHLPNOs5cb0ywQ0O/ZYXUn/9XZm2++Pjx6YeKE", - "W2y/tl0XT+wHEBMvftjRfgoBxOUTqanvV3NtGX/h+vXArIQ3qvMgu5SG64ZwHIPVGjAgDJiMj/7gxTEa", - "t9sFpCe3iLP+eOxfjf+gn6zn2epkPed/ffTd87H75fF//5c3OXYEBK9yDQ1hv7+mgk5Z/P1iRQ4oHs0I", - "YhaSOVRR/qhGYiT+BXrJZgDBRFDvn0OUpy2nJwdrxwQLJAvyyOShjJkgFwsii4wcn77Sk5ipx0NoDDte", - "0pgB18Vypo4HAdehpld6WT4r8P4sJ+k8MOFly6F5P5NZDsorvAO8pyp6T1QxmfAr2EjtAw+tOpcomeVE", - "ZrHBU1MREzEX0yHCmrzXDfvNWI5E9xPNkLoE1sFmhiPxukhyniYMGy8NKmROF2DrdzsQpwDhNp9TolhK", - "M7ByJVzlw5FwYC1CGju3qd6kQRUXg3LLe8Smz8lfJlIOL2gG9P3lcS25kmcohgIev5fzGpr0Brgh6OSF", - "UWX18iv2hnrCgrZTCAhE9cSHiXKNukB/8keT4s8/Fwh397jzGRDbxiQTJZxEuIu1DoKYxTErWL+0Hrmn", - "IxsW9EhIMRBFkjz+H/RCwplp1hgJemFq6NLhE+U0bxsfV2QKK55p3SpapzBhVzyS04ymMx4ZDA0Wnsxp", - "zrr2JjN7rJPdeh6JpV0ny8aZMKW2Nshk6SDLrtYe4fJuRTunBg7KHRlVtgvYr8b3BF7RMMyI5gTkaWDh", - "I93zMqirQczsS2Y6yyCNEt4HRsJcfA3Gkh9wdKxPnz+ISIKGhXZe2GaWnsKbYwlNUHgU7IpGObmFowj4", - "f7aBksgAM6N6QN66WBDG8xnLzGhlRjxlOCTHSeIwu7hJBmY3xP+x2xHWNTYGb3sxs2XQdIbgCjSVA0O7", - "ucoMK3cRr8iAz1OZ5eiupE9gvSnPZ8UF+MHKlAmMZJHlzwc05QeXTw8szMvn0L6DkKrb23x2sjXsRowf", - "WL/O+uUwgdVJldNH4hqs7k5F1nqoezZ4yciDq8WhUW5LMuGdnzfxuUN/u9IZxhgj6gf42v17yym3u/p3", - "wMrQGEIZKxgE/n2w4elBt5qXexNaPaiC5ZTevKdhfaEfPA7voMfhfrz1bocr2nJ/QeeAZyhBSdEbyEe6", - "UOQIMUzBG5DU3frafPT+d5l/nu3wptkVTU6p5CbM3UY1ugn4SBWgycjk0uAH7T7J+/b9+q7lFec5VvrL", - "1K/sdyH7ZmMTvr1eT83zwl5cn3wybr0LnU/sfqbLxELd+pkydO51ksqAsfUmxlQvQxGbU9It4UjIN7cL", - "vRu4Gdw4zVC2sZPqv5JH7wS/ZJmCV4h3+JLzs2/tgg9nMsvBac09h2Q16JSlEHD+s83h4G/nvx8Ovj0e", - "/PSPf77+5XTw9l+D/5x/evLVZ//lBigOnAvqqWEqVoTV07WJYWHNA9h6dghYA4tB5qwOW+iy1UhR63H7", - "JgndgTVImNdyCw2/kUGiw6puy0YBc7MHCwX069snllomjGxs2STxTtAin8mM/8l2HeL/SkBsBkQmaxaj", - "ePPYRrD/UTjY3x/c2vH+R23x/u/gOOolyf/hSqs5mpyx3CSN3gy429QiFzJewF0GDr4WbYmZXkhKF5Di", - "WrnuTNZ9xPK0SdchFLm5xZTzt0Hy9VNjejpFEsoBn2GrYYuGYB/biW6CMBoKgzsaTL3JYLrtSaY2e+mQ", - "YJZ0FGaDvVcIno8hNydqCgwCGwlzOWlOtKuw9lyb8b0TPD/R9Zuz6iwRKcsGuiPMGlrJLIbZ+kfmGX/U", - "Mz7aE37F4mq9PpHZSIx6STIf9bTqSqT8QIoUG3WJRVxyUguiA543MUH0KpYhvvfgYuE/XAzJGct1m+9F", - "kSTv9U9RwqhBFr8yOescKf8DgXdAA6OXjGhGLkQ0o2KKc9yAMrO61LYQRpVGxgGcm83YBnGlzX3Ww9Sv", - "q72HPFS3Lw/Vl2gLa2fiJVgvm7H2kgZXMvwDuMk2wE3Ci61YlptXg43SgMBjUQHN3GqNZQ+A4/Vyo8P4", - "Xpm6b0H2g6cPv0jVcqyG5ARDuUc9NBuPekRmes807mCjnr9022jtizXNZzRnYwiXCxvn9XcC32vm+a6X", - "OnP4+VWfrWmG53XrX+jaXhVZbczHVZaqEH/eLmtpahEgX9Ccbih11UZWyp891Y+dslj7uFjr0t5GnKIJ", - "yMU7fW4z6JUOzXJGFaEk4eIDi8vbhqOL0DT1peGHRgm8nmV8HSkOj+EMW9mEcKxaJ9Y22K5pbQR2Jic8", - "2fBuUW2jg+41eMwBR0RwDQIEC171fk2x8bA74S3Q5l+mdlMF4NRlazPuKc1a0zMB8eaQm+sDs4cM5YBM", - "bM+42BYaz6zySJTgSprMjzL7MElM3o91yPzNVgxTaru17cM1nYtpCc1nKWrTuG4CPSL7jsPbla6VekPn", - "5oo30NBKAaRpOnbpA66hrkKvpWlaKigL7Oz8Neof9XyYNRjbiV6bE63mWg5eXOMwx4cOqVhLl8kpZb3e", - "+xUNVG+hkV3KjPL7arllyrdEXr7O0q9cbzvFNI71fXb9ZTf1ls+sad0Zyu3EkncOwIheIUhy6IJgp811", - "1u8Z8PTFOlDWWAMf+M6bM4afl9BoRtNfTqxt6GHzucbmk2Z8TrPFmM2NzTyAaoFFCBRp5TBvYU5NhR+g", - "zRDqk6JTNrYhKGulv7cmYdPtO93SsddQk99e0zSFK6/04jzBaMhikw/LuOE7tWiCk9CMZGDnK48nUKvS", - "bWhnat95HB7RZkqnxD/8Aq7YdwHF7gF/7k7iz4WPk10wz0ox3lyCvwTh/UI3Nb1uLbaiUrBtTlUTbI+G", - "hozOU4so5Hs8k2OjXdRHnkczk1ZGmbeD3CSrjfEZ1J1SMW0tOc5JwqhCXAFsBrJXIuuta6UCNDmrmaph", - "+3YDLsfYazpYpZkcZ/DsOGZCa8K4YhDAt62wUSDN5ACr6gGY2t49rYZGe1oWtz01LQdhITTUt8ueM4xv", - "In7WHarLJW2O5wcVAhbDk4Ve8zSFlETwRuUQ6NddWUPWcZqapn1D5LHpwu+BOOKay/ygPba4J1QYIciU", - "tYPoGZ4ag0AG5ps+vgxXJpyCR4qJDC0hE/gyDJAIUSKLmAia80ub5tVlfdLTYnWSSbSEaaCPT18hJpAa", - "iYUsADwB8rDg2Vf1DToRvrFDq31oDYPt3TJU3sFKwnTJf0oh9GBd0qtRcXj45Gvirpqnr3r9Xpnr6XB4", - "ODwCN7GUCZry3vPe0+EhJIBKaT4DbvLdigBTT/9xyvIWZFaaJL4rPgIicSlexb3nvYSrfGBa0V1YSPvW", - "42lZ5MBzs+VSYBj8535juSGs3xzMLBC+lwkX3WrJWZGmMtOHrToOAM2YhXfg8Xv49wNb4A+aP/Gn0m39", - "PXlktPlj+FL6sL/XzWwD74CUcAcjsRbeATygpwk8ghrdzPUs/WFABFAH9HTHvX6vzPG41FfdgRDAK8AC", - "2HAis3lgNUw03sr16IXpmlhnuW6Uaf6Dm5o61WyjjK+dR2bMWPqmdBm0/QNLPzk8tFAHNntXPVfm808d", - "KVkSbgBKpqPz9+d+7xlSFerMUX/wPY3t1gxVjlZXqXvMPTt8urrSS5ldAIYJaG5VzOc0WzjBx0XWuonq", - "Dfx3TzcZTFViQFW1Ur8aFPpC5Dx89FnJ+FDVTFggaoSCd1n51FhVLCiPAxszYs4Y38t4sbU1RToqhoTP", - "1Q3MDKPGVUfb5aoQA6F5wuikL5B/7BJjdOOmDPS539yrDj7Bv6/iz8hYCQuhRpzJSY4xg6UFY0F43OQz", - "LOT4rLaDgQ4DH12nwkz3vTqfdNVpJgygqayehXKpQszll8EAusaz1TVsVrIaxzRX7Bp6J3iY+ZHlK3hh", - "yvLbwAiHN6Vf7iZb9XvPjjoM5UcpWI0HSw65zp5XBHgP/f/KFDptHIhnzT0x4fZ32IAnXKcd9sYkwFkw", - "HgTBFwTLrrvcvg9oFs34JWze4XPiMRbwpMbcc5tyY9q6V9rb3Evvw+HAcUKFDXbFmGlxkXA1a2fMUyzQ", - "hTFNWw+MeTcZ03HCThgzTVeY5uAFMklYTHTZNuucbmYrtrmd8lSa3jfzCq5Lk1OO9YfzADMcfKJpam6+", - "7VccUWWLlmtOmnbTRrrD26yLSre+oEJK0/ughmDdYUU7cpNxSzPvdO0KpiwH3q5oYDf+Pe5JBOGLw8rH", - "62iHzwMemdd8IYhkzOoPAOaR4MH0D6b/LnMNkyVtVg6vhmadRQoHOUQbkEkiP+oxejmRn5uKv+ui539H", - "v7LtPSicOHL2/ahgXTDv2cZX0QlNhVWuD56RAPJe0MRuPStOTWXrB4ZzWs/x7iECC1rCFlpIQbSNOjOP", - "vOjwopl1IYuMyI/CVBwJW9P3vyVpkaVSMdX6uIG1B85HeJfPHM6tGPrc03uHcz/1aQkxerXEl/8QUmOw", - "G+H7g0+2O32pjaTKBxfWsWvJji9VDugAyvhxlULxUma1gXCmIMAgY9az0QVMCq8hQMGM+QTCG3Lynk0m", - "rIR6ew8gc223F4/uLufVcsjXPbS27n/luLruf2WNiwWZcOpU4ALdkJbvhhYj+HddE+Kfz//+7uzFFjdE", - "qfLvNXld9sP+7bs6Gvq5utP76PVuCDXZ3o4CWvnAbzvkDWXSuiUacec3Le7nO918LYfued+1ZAS3XPvx", - "Duy2ju12s9FinIu/kQZvvrbYTi++ppNtecbBXbd2//Vc4x5uwe4WvHLi65dgW+FiAZE33a7AH9ji/O/z", - "xSC+GADI7tbuwIaa/V+BkZB7dwUulUNIQdmv594eu+QKCau/y7tjNcZ3X7dGM9TgfdHEHt6RmyKmg1zK", - "GC2bkr714Y8NF7mg45vXX5fjlm36wf1tu+dzHGr31e+HTx5Tlt+eFT3ciwa4J889a3CK8Uire5oplu2X", - "WXblb7bRdrUfZn3wP2vxP4Np2epeeGDM8q23Nl93DmzhO6tDEfymnTV9MJ57plLdA47JhLChfo398/k+", - "GWrXejYAALVflbsObz9o4GUewJuJxDrq+ICm6cAiea0jSQNX8Q6JVAuQ5X7EqQGVFvSvCmNmPkhTF2mi", - "aboDiUIgz4NoxqIPssgHygCAd3CE+N1gcJ6YuuQM654/sukWYhmpIfYA2RZMdgDluns8EkFgOuxDEdpo", - "HHGhZZKwCMAubMKBOctnMq5CL2bobWHGj3ZkMz7jr4G5VUc9xfIiHfXIXMasbwCNTCfKdYHJLtRIfOT5", - "TJMUzWg2tXkb3Hrx+ZzFnOYsWWCXpiEW14l1uQUs4tCkyIusmv/RLj9My0uZkZlUuik7g3ZAqk8yFvOM", - "Rb6h36BtObPzu19/NmhGbH7B4pjFXv1CIT5LlHAm8rFiUYaY/lzwnNOE/8kM7Orw/2DeFrLIRsJTHSuc", - "V1g2QGYY1Nntbqjl2skC58pYRc2ADRfv1zh6nKZLaVNFkgePRFDcVA1V+rIsqjeo043ObFGYO9Hoqcxy", - "mnTX55Y2q8ZOob4lEdTPO8UmBWTSd6qmovmMtmlpKZcWyiSfMZ6NRFUbqj7B/B/4uQGzSUVMaBRhYn9d", - "AIEqGZlxlctsMRyJNyJZGF2ntKprIEjXIU25snDSuSSUKIcprXsrt47Oaq0653dfqdn3OBj2rVRtYQo7", - "Kbj2qg9qrpOac2KHYkHUNrUdnL1WRyTYlz4sDVrMpr/zNcNflC2CyfBoxsqUtywmVBHGAZZtktCcTBiD", - "XEqA0jTA9Ei2i7bQBqMpLN3b8vPYqUpp8SMxM7XUmWEtR5KK08iAvDfAWWMD6geOn/DBgWd7H26Lk4fH", - "Yp4nh5mri4VN3rbKf/O9cebA4ue/S/bH+d/NBPUx6fH7Lfp2IH0dnTmrA//hKtWb8qRIEoLobOi55xKf", - "xi7FZI0lTBIpjxMyRpNxzudsDDL1/jkxrYOUApF/0RxHkwGkyYZSbcBRDKpW5mEdaEAzJ0hAKG1ulSF2", - "6vECpDy4qq5wVa2p+VU7zDb8VWt9rj4itir+L/FQaC+Oekh7dvQBGoKHOfhyd5x8DKdthbm7HbEgyZo6", - "oPH/FSoHw90S/A2brK0sbcM8rKPmnH5gBNNUeqUU3r3Kk9hImJYuaEJFVD9OFIoNIqqYMlsHAsRHMtP3", - "UTxehgURGh2UHd8pSYTBHbux7Vkma9S0XLUqZe74nSoIeNcUmZsXbiNky1ECSE0igygB5U6HkmZbvlkx", - "64ezneM5sjIKfakDiGOuMCFATucpKBuT5VGVySogTTCU6upT7lrsfBx/QXP2ls9Z8MqxheP+jyxHofse", - "x79vb+6KsjA0qXZVYUvcG4iGinGiwrk3ryUwA+sKY0s9WWvIDIKqwTR345phK7E09r7vD/cmsI29TPT7", - "j8QoaXm4nK64nPp8chM3U4RK9rsdkuPK76VpUwEsWZJI5AK9H6YZSymP7Sm8djwfrjheQ/t372QNnH4b", - "DtVASPsmCZ/v5VG6zvP72iMPPkXlSqyE36qKafhYvT/Banle8Mf3JTgxdxec+3O0vIWScqBYnidMX4MP", - "bFrqdnuT8ZKDfFLGj6Csb99a5ETvb7axZEEmhYhZXJU649mAF0Em4lRyAW5PaiGiWSYF/7PWT657rrbt", - "Pn7k+WwkIOMvQJ0RJfFJMWOXTBT6ZBjJqeAIFSQcLSZXHk94voD8vPDmeJWC41gr9rOvHAaWlkE5EXdU", - "YezCozaubvY27fmZm8s9u6t312L3CFLV+sr6h1snkg19cPOaLs+oUBTs0t1uz34FB4PiOcZa5wUqCJ/P", - "CyCsT0BNyUROeUQT0DAZpMczjc7lJUyDel5VgGokTNpyVczLvw7JW58K9IUor7dakWWK1ToFwIWRuFhY", - "OIflFoDKxNw2O8BJkSmZrWsJqCzdjdkD/KW6HVYBj6JOdgGc7ftrGaiJwg1pKPDMQO04QLfKbgg1g0DF", - "uxP36MOd/FAO9BjGaZm4Lb4FFrXmVYYOO2TCaF5kDL1Y0X0V5+7euGZ4bEMc2zR53Zv0piug2VnUQU6v", - "BpD1c2l8rsymVPA/4Y8DU3dQVt0hG73xejaPSSbfaPCBY0nx+3Il9dfKAUK55K5BVrGvdN2tpksCBjvw", - "yq4uHUtWf09Xji2y752O8QtdRXbPyFWdiMljV2V+mDIIGsPCLSdzm8J2YJpc10N5zVMz9tIVEc2UvlgQ", - "ZTL6dsRBNcXPfwc3WnuIOdreMfwHoGwjB1rwpl41D90g+XLehOKzy166VecyK6H5fpsxQeSc5zmL+5YM", - "fdlTDrkfmgVYvffk0VwqLciR3sEnPFP54yGBNijU0DPOkphwRdJMXnJ9zbSBktSg//UJR5g/5WH1Dclx", - "mjLj6utjBY5ELs2Ybdk+MdGiCAdoQQVtOa/RG/YK3+Wh9JVZR+C0O3ylap4eXVZtp5Ks9nQprlH81kop", - "i/MJNhRoV/PUBc2jGZETKwelVtHcdpLIAqdfGbjJ1oBXFLqAMu12gIDU4ljnr+txiZ0RIBMms6XdAYw1", - "0HonH/laN3Xn+Fq3zU6oWLyZtG4nbb1sh7jzTueoJ02OeTtjljFm9JKRC8ZEuatCZGKm/2piC/XtBoKG", - "zAOCLFSy+HJED+VjHeGrnUnsbXd1qnhbsuVI4hraISquu5pfcwd+SBN/zQCq1QtRPRK68hcL5NUxj7ue", - "Cm15E141Kg4Pn0Y8hn/Z9s6GL5HEfdtlHRn3CjbXUx7NO9dL+3GjXPSm6VZnKPN9pzi7ZgR7ckoyvYe4", - "x3z68gNvylVck3/Cu+HBJ/PTinT0Bt3VcdmKZPQllauN8I6AB0TenSDybswxy7LQr+KDKctvCxMc3qR+", - "eQBfaJrRr8GAqb4Vtiejr3GhyYuRJwsiRYI5HgvB8zGk00BLkA3/w7Nuqw/T/nh3Vxb9TbbmGxWd++ch", - "tMO9HLKlHeBlodVdEe5C+DisBUROlp0ioS0rF5Dl6E4IB8w2TIQnGTcgCSdS5abbNuCft3DNBKLIjCqi", - "iihiLNZa685KBrKkVeuGy64lHVN5yTJBRcS6iYPt28B6obtbwlE+StxGayFxvrkZUzK5ZIowGs3K1wYe", - "M5HzCUckstJxDgx0WYn6MxKmQ+MlbBEoDQOw2F0d+yRNCs8UUw3DGwnfaRfIHL9gik8FmlwuGIkws7YU", - "Wtz5FajcScbUjMAz3yVNrEeIsVPYVSNcjYQuA+56trFoxuLhuEVflLPf6pez2QvejtTBj47euk64yd2y", - "QUW7WeTLVBIBmS85ZZkrTjkzbQ/TacYvac46vlQnyRz2sgPdbsbjVfbhlGUDvbOplEaMpBmPGHFVWwzG", - "to9B2Ud457y+be/nn1/rjeVU0/WlJsME4u+ZWfDnn1+bM5jHIk3u18X0+m5mIFzGu61Wwwbz7sh+aDj3", - "jekFib5pA6IvPkFmgzn74m2IDW5bn9lWadGDT8BfXa2K67GmMTKGWHP1hcTQ9WBs3ImxcXesBeu2Ynee", - "JvKCJiURWGdIbIAK/o7ZvB2rEtAX+qg+IVQsVm3iho4GqwWfDw0B23u822SDD7wvm4nYUspVc9nJhvYP", - "cxmzRP9We2GupU2v/S2X9/Ld+eH8dYvOX066t6m0qpvhkkcV68ZVo4dcLMirF6Uag7hf+NCqyUYiqMqm", - "rK7J9rtnHt7Y0e0+GtM0U1U56bqMbdlk2SaMZZb5c+/UdQpr7MBxik6nGZsCARVPquM2R6rjBz8qdxBa", - "tSpVLypTeqM841B38IEttnfoAjHZeyArUHHP9mWnLipentWs4i3WDqjaataArzt1hQJK9+QIBX2HuAIV", - "/hdvwLCr1+CK0G518An+7WqKaOEbY3OwPa8+M5lOH+wMO7EztHLAUqclqGVO08Gz8S1Y3sOb0gL3JIh3", - "CaeY2NsW36IWRWAchPbDKbtyDlp/s7oxNr1/bkFtHNvxdlbud90cHqxanMjMZDABbHuWk/fHUcTS/Dmp", - "L+578si7tTzWV5ApGjDyrIjyImMx+cfZm1/8832lwZxd5QeRunyvq8byo0gkxUO+onMGGUX11YiSk7N/", - "EciKpgoOA9dkjoRKM0ZjNWMsNyk0dcFIJsVcqL6+XcDtp++udO8nmZz3SS77xEbf9s/J79YbY8zjvnPN", - "GH9gC+83Lcb9c4LhGDGfMwG564bDIUZm9DFzTHnXM+2/N/ToixrDiFZ0S/w4Y8IrxZW9DcFy/UWNxPtp", - "Jot0fLEYl/29x3Hms4wx8t5R99+2GwyTtR3lcsogNZTucSSwS2+0gW5JuNcWx467ov+C/l83rv6qnmD9", - "npUPXZld0XmaYMc/6hXCMOiKI1G5YNBxuQGuLt/vAfvqm7EWkVz2faGoyERVJMDI388XKetDCyPx5PDJ", - "08Hh0eDw6O3h4XP47z/92h+P4I+HRz9+/dV//vbVV8cvfzv+508/HD355d+HJ//77cuf+jSaswEXUf84", - "mjPySkTD/jTNB88GeZFdyD4XaZH3j540ejsK9fZkK709OWz09iTU29Nqb98//fd/jv756/G3v33zr7+d", - "nj150Z8m8oJd9X+Ef8iJzNJKb7LIdXfP9D7yiyQgjoOLRevqtpRpruja67Pe/K43P89w13PCYcJhVZ5x", - "MX0w3fo+URufBNKEig6RrlCsxVKLTezQUAsdbOsJ0tlrL1mm6hba5bGut8RCumI6tmfNPNUd7duYqYm4", - "Z7ZMK1DNB5jTTMZFlJMTmtNETjfz7hLsI3TRavLUH3dq8dRrul84ek1BkIESKr58q6dZwM34J7A/HHzS", - "/3R20dJzuDzq0xDY4T0Z+n0wje7ENHotNllqPl3GAlOW73/9D29UoTzEejatrddkvuUW2WX8Z8yy+2DB", - "XRhlFcvytffTm2X/O42iuLkUGG7d2WZ9QON4JXA7jeMBwKQrJSMOxx5wFaMtR0R34xuY1m9OhG6jB2RC", - "xbGeh4c0YsuBlC2bTWR2bc0fTuAbx5AMDPrBAMqVdxxk4S9/EygvVcCLe7pZuf7b9gH4eJ+yewFPOobc", - "tZ7H32GSV9zUfmVzeck8cZlkct4qMN6V7cYFpt/athnnw81wuyxrWcPji53dEh372bMH111CFH2YE+3N", - "8T6w4eFN6+X7kjwuzHK7vI2uzefeDfWOsvouL8Lrn4FuXNbuXVqBXUjd8iNRFs34JWv3aDrGAtZWZB4D", - "m7JoGrpvFst75EtnOcHng90wZVpcJFzN2pnyFAusZErT0ANT3lmmtJywC6bM5IQnqwLkL3A9iC3dYgk0", - "xQau0a24gdwEoyHB98yzoL6qQZYyM0TOWJ5zMe0UQyXYx3rj4Bz8fa1DoteQGpcVZTqAs4CtbVJ65plM", - "FOHiUvKIjcSUCcN7Q3IsqmmKIiowr8G8SHKeJqwxTBKzCRcsHpLjkah9JFyRhIsPGB/pRVbTNB2StzOu", - "KscWrggD4eJqxuKRiIvMptmoNfwXhbYumyk5Y3PKhSoTn7baJmtCtVNXjKo47Nkpw4w3IH7VEl++h0ZQ", - "YrpKY1ifH3ziHR00QoL6RiQLoopo1hQeA4QbGysYpAMvHeuEzCuByaaa/ZRyYfAEqfBciwulu3C/OmAC", - "XQ1c30GgJ1zQRE+7VQSqzTrZFJnVhyL+YD3cjV8J3ZSxl/qU1Lk2aB68LXxwuD/deF8MeZsz2XLfkZV8", - "Zsxze2S1XZnPrnEY2CPD37+4P7qdk4MqLty0rsifXS26Q1//Skfb8vmnHpRYzb2f3mtEsWB8QWUJthhO", - "cOa3u++wAp+YB+ed5c47deFvqprK0lbsBcELrt/gjm63Rvn5hO0HKTZASIi1/O933EdGV/h2dYUTKSYJ", - "j/LwBbrGQqtZcsnWd/DJ/7UKvte8YtR6Xn3oqzb+Bdw11uLVe3Ld2Cm/dfLU1WcPLIf5V/w2WqzzfpF1", - "/HW3y7H9jc+HdyUFts8bD+7Ca544iGPclUK38u2rkxhW/+47VG7ky7ZcTus7yhoOP1uX0hV9fAG+bg1B", - "W7V9Pfi+ZWtvbdeVsoiKiCXtXhAn8B0v2xXhIb/xJNGLpa/fXGjZimYsLsDKEZnjIuETXTNjhGZsJCRg", - "21StCqaWuY/nNMsJneiBQoIz6B1nJ+fz0CsdlLgV58CbuTfheu3HDLjWWfRuWwKveW+CVdzxOTaaUTFd", - "4nV3kkjFFKEkK4TQUlvd6EWM4qjM+6gUkOJJZmAkyyWmP7Mv9MYd4MTk6VQsRmSydJrRmKk+wIXZn3Xb", - "4EyDJAb8WfDDPRJrXKv9izUScvdSl920gMM07ljAC2E3z4G3UbYL/DtXvrmf+1tt4JUt3NODweVhk2uX", - "gZLdWjltA5nI6dUgktWMfwFrS1lsN+9jr0SUFLHniEOvCPQXgoDqYjrh2ODYNNgLYK1dSJkwKm7WXPKW", - "Xp3I+L75ZrrlDHLoW3rVlseyNUI6+PZiuXSnXoVmBffrTmiICB4o8NOX70domeaaPNOi7w4+5ThRjYDi", - "oBOex1qrN2nX8oMT3k6c8LbEGf32h7DbstyHe1Ac98RiuDUmMi53dUc6xbK98tGuHOk22f/2wcYPyEwt", - "yEwwLdvaXHXbLLsM5//8WUY06fV7RZb0nvdmeZ4+PzhI9B9nUuXPP6Uyyz8f0JQfXD4FwOaM67YV3rkz", - "c+cGl7Te894333zzDSx4I7QQ48TwxX6K96CyS/X84OAT/v3zkKZ8+EGK6eyPYSTngW5NA5WOC31sZaKY", - "61nCX4pev0f1/+YMncjOQ4SVM3qSyCJukOWOJcNIf7czoeXXLErjQa7MD88uaVKgLV9OXLiBIrkk0YxF", - "H/S1iWdkwmheZDad/LDUNsF08oExeOFIg4RdssR5CUZSTPi0yJyVo9HyCyypeq2LRiIM7iNzKuiUKQSI", - "7VtQJTRu4ki8tx3VeNwZXFDFYus7GiSmHk7YpMmlCYxpTnWDBJO6cjElQmZzE7CRZjzSf4IEDZqQhIpp", - "oS9qgAavCI0yqRSxGWHVkGCqWkhOoBYiYjHij7ioLnaFgkaULDIoKWJCi1wOYJKzOYsxY0I+YwtCpxlj", - "wTG6LIYBD0hkBEUylmZMMQHxLGYNUnrBE55zpsgFjT4gWD7uVn2TX9O6iqYsGxSC5zhTq3nA9hsg6a27", - "5euJsV6kEU2iIjE3AIZL7dg72IXWV83WT4osYyLiZjaBXL2oXVos6wYats7HLsbP8m4gUg77tiE+HXpu", - "eDY3+z9OU0WYgEQgC1noOdP8ozlGjxOa53+yStgh5DQhH2X2YZLIj5BVUCv3qV44McUlLplwoXI2R9K1", - "dsf80dBtRAXw5RzhP2LCxAzU0UIWZaAjiyS2oftR6M4MD5k+o0FEFlUgELNMCv6nLoKEgmgBUfmMZ/Eg", - "pVm+0Lohn8hsrvqOSeDxRLNJn9g4SjPimCX8kkEEo53+PplREePy0MVci0Akk4TB3oGqBt9UbfxDxhKK", - "5ij1IbxcelICS/SDyHmeMN1Fjbkx/NOoY/2XiZXL1bzhtxpyMq+8BPu95hmNPpiplRNcKyv8WpHiGg+r", - "tkIbHMdFzC95XNBE6cJ+fKrCiDld0CjjC2bxwJB9INatOdjg8KqWypBA2z1uk7GVtW96XK7nwJigBIjM", - "ZYNXuo7sh7JqmklNEosJtWIlC5UstBxq7WRVupK4k8zpAkIZ9XTM5yzmNGfJgtBLyhObaAhT41R3VUc2", - "9t02MOVCAmbyIwRKmvSxzI63Hh5NBU0WOY8USYsslUorHtOUWTa741g0T7eHeqlp9ThnMsalguwgWpUO", - "yWtbdl5t0pjKNDEu/RIQSCCzCypbTeIkYVf8wjYAm0DEBM24VPXZUb3P55//fwAAAP//zQ0xy6NMBAA=", + "uY3vqqB1Mix6HvbdJQMvX/T/Y+9bmNu2lf2/Cv6aO9PkHkm28+hpfedMx3WSNqdN41snp3NO5JEhEpJ4", + "QwEsQdpWM/nu/8EuAIIkKFGyZPk102ksCW/sLhaL3d9m6gpaOq2do7DNbcFR8dZ+dWyyi371JDBzxKy0", + "MTlRaiQsWkW1K2OiT9KUlayE8GVqI7AiTs6N7Uv9dA5GQXYBmj9+P+CoaCw+n2rmSHzEJJdUWtTRBStq", + "z2APaeqN8t8IzeHr7qL/RuiqOXBqFjqsUrPA/JqCbzOL6jZhq2wpmQxRS7JZQ9FLpjXbHGyrGcTe0zgC", + "F7moqo7p0Rl7pKQz1yAt4pgmEq12BvDVMBcMu4vkgBvnbG9xTVYLxUIyZXiXaz6GG+VLnV88EnbDXnwf", + "rOeeea8WUi1WAKgGU8rrafu+kS4/wMNozAIk9gGHnceVW0SPH3EaAABUEamGHB1bhSMFSivgE7RVPNZ6", + "bNPb0/cE3BZNmSLKKRSB7IfRJILXFuOsKtX3+cx4Ne6pAqrk3vf7+/sHL777rnfwfM++D++lw0iKoeph", + "aDL/DzHGoj/NZk8NkHjZL//k4N9u9JOeRacUUX3y5IfDwSD8G/zTV389/eHfT3/wfPvO++0f3m9fwbcf", + "PL/8vELbp09/ePrDf7k3yuoq++w9bzUM+usLBngUtxJcAVwyIj4pDXYHyApv+QWNo/DERIQeT0XU0qWh", + "cvxDRekuQBWitMa37lQNfpAurl3rIbqn/I6pvXcbGiuJRP0IU9SFGBizPBg61KqpPPaY4SJcuiKaViLE", + "jWN0g3/PWvSAMqE82JEI50sNi84cJLADjLVrt6PNnr9iibp+8myNbQ9N3erOV71AH97m26UZQl+wFy1X", + "ZZOEgfRg6cPZsDa0ocNrTIjESrRxjf3TKPulyt/50Oh9Gw3A+fbNeIZh75ST75xQkK3u/IxeDWNcMUwp", + "gHwBf+9GHJj1bLXlmBXgxrfcJCNYe8vRRcDgbd/cbkfc2e2ID5Uql+kUKcNYXLI0oJLpz3mSlD5jan5T", + "2hJKxHdEKHoX2hDKaUZ5SNPw5mhkC2J9YRoGnPDvsOEbXO02iyvb0KJ7JxSctcks0bSHX7urVSxLiZVr", + "l46VVWs7SuqqVcu6Dji5lW3Wy9WSElWsIzAsZXQ7kRyariIJUJvfvtB/6zcy+KAup0ONWxHJoRGG8EGp", + "FOYvQN6BvzVhwd+aKOHvPI90v+M/Q25GAJ4e+EBcpNNSKwPm3wLSE8tD9GrH2HjkMGUTdoUIm2rqulMD", + "/zXkLLsU6eeh+zo5/EtwNowjmTWVDqIwHY5iEXyuljAR2apf5wq4jnb166/vjoUEKNh41QfEX98RrOf3", + "3C6XLl7yCptIyOIuYf1Jv0sGnUmS9V4MOurPIKZ5yHrPey97UnDOskHHG0jkD5D9zQmMrfTx08kH08cx", + "9EGe91+S0+Y+GoMnfQLMXcsTjCNbFQ1VfHZi0MD+zCFeVc8jZFzM9DU8gYfwz4x7DNE0mLKh2vthwtIh", + "lNoI2Kdql6h2NaiyHQN58vH01VNwnsbOL9MoY9voHRpe0H3EkzzbbMdvVZMLuhR5tvE+30ObCzrFg1Rj", + "/G+u399Ns0QsHkGVMyrL7lmVBSzz/oKlaRQyzJqwItPg9ihmQUQIiDFIWNpTjCoTGlgIcN1Ln2hY5QST", + "uoAs7SKPDThk/TTRAOTtWLGgqUlorKh/TtDlnpRjQitNEcrDAj+yS6KMXEZxDAlG0dhaa976+kL7pSo6", + "VnsT0RJLYkmPS4iX8IBqwHY3lyX0D3x4iWQxeZPmzvZW6XtDQY+entlVEqVMQn+wdV7g7mPKBYeso7i9", + "xYuC93DChvxHVMXvDttTRb0tJc5Z0mr2voPI80hROmwQ6BITJAEVe97L9S97F4yHonyOLz87bbvOGhdz", + "KwVwVuhtgeA4sdkCVnjv5QUEBAqGlAUiDbsFzIJ58ccyVvbpB2EDnKsFBrzbOV/fGF7OsQHAsUAR/c0A", + "ia8iIMiTIu6Expd0Lsmg8/H01aDz1DuarcoS3E6PIFk4kI0LFhyGkSoQzwWBwOaxGX+Ht1yzdIvHuoH4", + "WX3klYVWraOZvQesLGiaQruNdKij0N+cWPP2Xoi5lbo3UtDnXKN/qknGWu+FHWXFvqOAnWJdLwFafFOg", + "sEDdhBSbecew4TfvXx0omkUCyXelqp4NTQeDXraaFGmGsll2dpzajWi4rkrFtikzFzHn1CitbmH45jlV", + "40eUOK9feZWQVr92nxTrtc7Nu1BUzcVYJIzTCG/GlGfTVCRRsPatu97++4Txo7fY/tHC9le7cVsUsSYf", + "Jgfs6tvnVawrF1ad9v7a730P4OoHX58UH3v94dl/O7/+Tb/HL0YnwYERmYmUWUc7gMHhBrrJTTqSmxzJ", + "6H4EVxhbEH2FUkxuy2gaTOH3IBVS2sbmiTppBnzAf2FzWWROHhM0zJOD3rfPnfcAjOPU3tQIRAKuOAMI", + "wsWN+iw4Z0GGH2ZMTvXXaufA4WrQGQ46/QF3XS6+dBi/6Bx2Mia1G1QJQeKlgyChd8+3r5HMILO43GwM", + "ulpKWDxMGr34MrVOwHm3JWpBKUy8FX6eJ7y8YMWVKxYB1evM8mvTliXJtjYsSeS6CBG3Y82KgPRNrjim", + "ldzSomM6Ks+6FyuxJXgG7JiM5q5z6KlNp2GC9wlN2aH6qUfOtQJwDh90Nmj4exxxGuOfGg/0XFWpRjS4", + "bdqQBXBpTtC4ZGv3F2yHkNmPVG5tQ5TyMVLt+1CEaDZcLTxyyTXPbIbtFIAqInVqVSxEjctRZBra1opg", + "ZBZmkLoB21gZwEOnAKvCh6zKHvW8TKfYQvv+NZss2wonh9F2NyRzOtrEtjSlNlu0Ou4YdgXxUh3DIpwX", + "JYWw2oCX1o+siPLibcQ9gdYgS4dyIPvhqhNXXS8iT9ybaGuS07bvw5wM11MI2hzoFcLFtWteBfTMfl0g", + "qhisA3S5faUdbFcN87Qu+Q5Wi3E3t0E8/lRHzeCrJobB32YeZyvnSffNuynka1EIXSO6qbvIW6O0xgBu", + "m5Q7FeMoNtlBb/tVIYkpH67ba5JGM5rOh2ymoaBWbgHhN5xolaGOVllzTE28B+7nW6KJSLu4EwadNEeO", + "DVc19LY6i7BXk2PYQaa0YuHtq/6qMI0rnIVF/6YfvR5rmR7XmDIAmwKahfoagrvMEPqdNayxa81dI7Sj", + "gijSnc4eB4BjQW7a9uSxF9RCdG6krU4c56q6WkPzWWuKSzQciyi6DQFj4ri8we9riV1w3BvepIGlaeFc", + "a/2W5HPZsC+3gkjZTELO1UQnGWX9SZ/8gLbgT+a3s0/sz7N/fDx9VfVS2MaA0CPh7avKWEy3OBbw2Ku7", + "OmxvPOAh4RuR+uHsUyB4RiMuYWSdtd74VhuVfekvj8l8jauETytbPmaK86U8FPwSB6JfpBoZDQLytsRh", + "6AfcKJ42vh7a73g0h5u2K5W22BU68DSu70lMt2V1UTr6Vl4z7snDxGk+siu5pS2QbhdLNfwVj9S1KsHF", + "bf2a62y9pZvNb+M7tvp2HWmceAiQKSUVwtfXkI0hN+5UXJJMILQGvIvahAfOjc154TQ/Yy8yrzggQCbO", + "Fw2ZOCsjfEvQX+WjBtEr58bQ7jid/+obtwi9Lv/Vhz++djswwKH+OknFLIGAghCS0v307cv//P3ly6M3", + "fxz98vPrg2e//Xv/+H+/f/OzzvBz2AG/NjkEeGnz0K5daCT5oL91HUUWTQ1AqYvoCRgltKTPYhdU0l3B", + "tkIZSODIqen3OHLaBqVcbW0ui4QSOrnB1+5j/rq7n79uI7lkfqMzFpJ/nr7/7YRmU8Ku1IrovDKCsKtM", + "DQkdvVORJ+qsB1p3MFvwvgkm1JoThk45CQ4Xgkswm3Jwl0ymlKMrPmLH8ZClMhApq6yDI3tqAqAmKV2J", + "4LPb6qux5gwDpKjRcRzmKSNpWNGyJF0cCszhRhPlgE+ZjLgGhUOhLqcAOmXGrwU1+SjZOIecrPJzlBAR", + "W6sbeTse8EpaPhrHZBrJTKTg2K1v8zRlpt2wf5eyJN6SjIG3PSVe/aiqLplHFlRFgc5SZARA2cb7jQRZ", + "YDMIzmGdP7jliixHMp91Cb2YdMks4uiYNaNXLitKVGFM9vkU4JScDLbajSyhqTSAmFgWun0jUi2DhoBw", + "57bcLQ8eB2UEF9VpcvvkDcDt5jwb8NLxatbBLqUaaDThaF505YfVBZaQz9rpCrslpaIkBs+a9Mmjshqy", + "ti5R+H9qZVAtlQ2eHZqP9GKiI9sRCEExOPjJ+bxEYYC3NwER6uM7ST8EXTtpr+Rat7hyHiyTt6sJA7OV", + "mnHTabpwsMCcCzLcDThoGzrr/ZNBh/2JjpwRH3SeOgm/8eCzYcZN+b/K2/F14Qb9lFKexzSNfEL2AyhU", + "tkApDR0oWiB6IO24QfzSQzQHuNJaDCIWZGic0czlxpMPB+86XfWPuvKcHLyC/79r5rdrAH0fuRTkYoU6", + "yhtqReoO9dzcofYP4T91f5q4i6VHqxYDQsU7h52PH44xrZ3TwjOnha+LMsu1v2GVOKs5L32VPDXnbDVJ", + "IuYCJEWKmEjaVKfoif+1soyrz9wl2YYhRX8xT85EWBK8IDgD5JloyAc94HYOlQSLeKPgWZQyN00stD0c", + "zYdlabQ4L6Y7JO2WQUbzEl1+6pSsC2croB1Xhb9Drg6Yu6bc+kBthkk1Olc9hWVV7RAqtZnGonm/Pfrt", + "CKXCf1SBVzqL84AD3uDh3t7l5WU/opz2RTrZUy31VEvyKQLdFk07ef9Dtf2ziOMFA2gOI4H96TFlU4rv", + "jx+OoRy0b6N9ZUMi1O2k8VzEJJnoLxPZa2HoviuJPgNT6Ug+1C0+VU/Skll0scHK2MLg1amX5elIdBxw", + "pCR3ErQ2Sdhm2amvBJ3DzsGz/vMXL7+FdV63ta/tvahwixCHGWgK81kroeDm3CJslmRzhK1HzHQNqt7W", + "xcrZ4C1nu15Pau+OF1p5jpWXb0MZsSsqp8sud4ZL1tOUH5M73+rkznqXN5Pc2ekA01bWuA67Wylxs0GP", + "RdTYNQ6riE+0JXQs4lhcmpD/41jkiEUrbUh/3RxaSPQSwwm8OM4SpfT8zOJYdMmlSOPw/8G0wP5RUpws", + "RwJnvwwO9sc0ZL2D4HvWexF+G/S+e/b3l73g5bPg+bd/f34QPg+KKNjDjs550dP2ETXcC5ZKnOVBf7/j", + "uHdZIdIDkwo6YZUkQOU1p/yk1HiitU0IVlieEzqPBQ37xLwQdEk0JtqaR6LMMT/98/T9b0Ro17HGvPoF", + "VahBQY4xnvnt38f4I9pyNGe4Ow5nL1Ipea9uzQWrDDo63SSAVf+fFHzQIZEccKrIx2juP3/4cOLeQKt1", + "FDEXRrHar41zdCQlzSgy3sKoZdBjoZh+61Qzo+GUpepHyBNgYbHzNKqZ5ZaOY2GosSweRcpmwJYkvsTC", + "LJdHbSN8B6SbU0fv5TSCt11Ng1OaJIxXbZQVfnLXp+ciwC0bncuH7jUIWdJzDcLCPoIsiSA9i+K9Kdeh", + "YMUUsItlAyx8PisvBfBpZMhHJ2MyqeCgS63GmKUt/aaz16UD/sTiTISFb9LT8lDLAmnJkNdzGl0GzW8R", + "OUSgfWVYQRrknZJDyDLqjvj7m2Py/Pnz78uzWCBBl7JQs4yiEZdESyL9gDoyJ5SRXbjmKYO8r8YKI9II", + "E7zwyYAXs6qsvJj19ae+FDMGLa1jmLd4DC7J65oFmZ1V8vCribzWXTYe7GVY+JURfuyLxqx82EOQO/5Y", + "yoFTPtzdTC7LdG7z5mnL3sgpPsQnjzWO8ZLz/aKajpe6p9gzLPb1+klwTEDO71bCLkmDE5XkkXQjGmB7", + "3QzB7u6sYAm0mqV/KJrLYisTnWCCawU3LHoAh+xnzZEM8HuUSTdrFVxCjKP/+kEHa4wKezOnRahtdOZy", + "EYWYv6MpM5e2DepiJkGXPmvKL62rGUCqguVfthObnWthZJmRlO4mu0t75uZ+1MNcTdRVR7Sa5IMm6ku3", + "hay+RQPXz+brQY3CdLzEQuQB1FjbnLTHmGu2fR5c1eU0n1FObksu3N9E9kbkPCyIoF3MLpXMUPKXWqKK", + "TAfhFZrAb0Ldf3MeouSSGeVVeO/Pgk8Os5QG7PDg2fMXL7/9+3ff75d9PG3hF/svvtrlqPbzxvRj1J7i", + "Z5MoCP7VQAyQK+jF/gufvfpMrZA2OtSRiLRHlfUQkUrvp+koylKaztVNM4hA39Y+EuV0QINB74dP+73v", + "z/72ZDDo418NmD/vnVRiOhPsB3qlqG/lrJxOS72YXbCY6GsDyegVUr+9gWg4DCV0UFGvFpXwMoc2zgKX", + "UIefGtXdoJeqK6ybE02n6VQLpA6VPBMzmkUBZBAv9GU3iVokF6CTbtbFsqS9GwdKP+oh5lwHiIRhRq9W", + "xMbQ+7hIJ3lV3SANQVsGpLAyyfnBVkGuuxAAdXaDo7R9lpMV6y9Lw9tmZjTHVasFUFx9nZp2eTU4uBM6", + "Ye+Y733G3sSSImsmvnE52PYmKS34UFsXrbE2W5Vfasq8odHqXUlYXLsslP1VTyaMfmZUznsZS1M6Fums", + "hz5WBZ5g9FdZpDqeGqu1hK7g5abWa6uyf7YqjNV01LAhjndQbVdwQVHyORsTaf8PvIKWVzrRp/4SnyTm", + "dUdKzBmt+aQ8vOr4Wy7zSUw5IJyt6sxl6lWPN30HUisx0u6cFOKCMKCBa8yzLoj/VMBRMODaLIZwaOBb", + "U+TLVIIiydNgSiWDi1VkmvSok3YqrQQEzMBKLZ2Iwvt2ACOr3fBg6/VArKoJRR/d+u+FW/84FbMhhCEl", + "ivzar1PJgdpLUp+Z9TQD9oAO0I+3MK9qujNIwn6mcKnvjapftLc5p/FVXcBn9Gr4Z05hr5vuVrgxxVEF", + "VOPO2/gT64mG1t8VmI68EalJ59ozlwYrRAB/FtL9FCiUOpMueCrO8jiLatWUKGK8gGTLOaSOZiExk6kN", + "qu8ka3KTrb+jV7ZSx5c+69FLvr2X/DJLjUmj7zU5tLLLnMCjQXZMMxqLiccg03Tb/le1y2WZ9dv5kuNR", + "VhdBXk3FnMa310+7UDR24qutur+9i6Nxp9Qgd7Q8aRSw27s+pSwLO1qgRdJhtTWqCQwN4AA48WAcY6FP", + "rYmkzNkWTKgyS/Mgy1MWGpvMpk2p79CMWmQagXlr2M7V7ac2b2FdqUioUsvxDRKKlV8cQajKvSQVeynN", + "WEDTUO6BQ8yexq75Bd6zGhPt6/SB7U26lRQrN2jONevkI+daFMjKroRof0DPfpOQBCPBLD32yfuEpTRT", + "FK6udLM8y8F8x66COJfRBetCAOqAC86I0GXhJU27stCMUA2eVKN67kuNI2YjiKR30N9DPUhpHuViMYEg", + "y6PfXrVWDurrVfFBX5S8ENgCLTgN0V1mxYgpV56AdlfzRbn+uaxFHW7Tsr2IL2svkgR2a+xpsqxsLfPZ", + "X7hgvO2K2VRVukLbmfLlS1dkwVJr2LrhVmuomr2JdRTpNbnk/e83xCTFgw1KJ4JtEA0H0kaImVC268sy", + "TtSMCViatFib0WS5aBvwimwjj6Ltlog2xC1e2iaUchow2V8fheOjcLx9wvEdTYiqs0BK/s6CPFWFTyAG", + "ZUXhaGubEBZcAU4oD6YgKcGoH/GMpRc09gkzVW4zpiWwEPXAy0d3nwlICaCNZJWhVqFDFnmnmcuNbhYG", + "0O2YabUf/tvT9999u3/wSscJN9h+Tbs2ntgNICZO/LAd+wkEEBdPpLq+W822pf2Fq9cDvRPOrM685FIY", + "rmvMcQRWa8CA0GAyLvqDE8eo3W7nkBDeIM668zHfav9BNz3Si+Xpkc7+9uSHw6H98PS//8tZHDMDgle5", + "moQwv7+jnE5Y+ON8SdatKJgSxCwkM6gi3VkN+ID/C+SSyQCCqbfODyHK05RTi4O1Q4IF4jl5ojN/hoyT", + "0ZyIPCVHJ2/VIqbyaR8aw44XNKbBdbGcruNAwLWo6ZRelEEMvD+LRTrzLHjRsm/dT0WagfDynwDnVAbn", + "RObjcXQFB6l54KFl5xIp0oyINNR4ajJgPIz4pI+wJueqYbcZQ5HofqIIUpXAOthMf8Df5XEWJTHDxguD", + "CpnROdj67QkUUYBwm80okSyhKVi54khm/QG3YC1caDu3rl4fg8xHveLIe8Imh+SbsRD9EU1hfN88raSz", + "cgzFUMCh92JdfYteAzcEmTzXoqxafsnZUE1Y0KSFAEOUNT5MTazFBfqTPxnnf/01R7i7p611QGwbk0wU", + "cBL+LlZSBDFvZpqzbmE9sk9HJizoCRe8x/M4fvo/6IWEK1OvMeB0pGuo0n6NcpI1zS+SZAI7nirZyhuX", + "MGZXUSAmKU2mUaAxNJh/MScZa9ubSI1aJ9r1POALu44XzTNmUm5skvHCSRZdrTzDxd3yZkr1KMotCVU0", + "M9jv2vcEXtEwzIhmBPipZ+Aj7fMyiKteyMxLZjJNIY0S3gcGXF98NcaSG3B0pLTP1zwQIGGhnVemmYVa", + "eH0uvgXyz4Jd0SAjt3AWHv/PJlAS4SFmFA9IW6M5YVE2ZamerUiJIwz75CiOLWZXpJOBmQPxf8xxhHW1", + "jcE5XvRqaTSdPrgCTURPj11fZfqlu4hTpBfNEpFm6K6kNLDOJMqm+Qj8YEXCOEayiOLvPZpEexfP9wzM", + "y1ffuYOQqps7fLZyNGyHjR9Jv0r6xTSB1EmZ0gf8GqRutSJjPVQ9a7xkpMHl7FArtyGecPTndXzu0N+u", + "cIbRxoiqAl+5f284yXlb/w7YGRpCKGMJg8C9D9Y8PehGM6GvM1YHqmDxSG/e07C60Y8eh/fQ43A33nq3", + "wxVtsb+gdcDTI0FOUQfIJZ1LcoAYpuANSKpufU0+ev+7yD/PdHjT5Iomp0REOszdRDXaBbikEtBkRHyh", + "8YO2n1Z/83591/KKcxwr3W3qls47n32zdgjfXq+nur6wE9cndxi33oXOHexulkvHQt36ldLj3OkiFQFj", + "qy2Mrl6EIq6bk93nm9tmvGu4Gdz4mKFs7SRV35InH3l0wVIJrxAf8SXnV9faBT+cijQDpzX7HJJWoFMW", + "QsC5zzb7vb+ffdrvfX/U+/mfv7z77aT34V+9/5x9efbyq/tyAyP26AXV1DAlK8Ly5VrHsLCiAraaHQL2", + "wGCQWavDBrpsNFJUety8SUJ1YAwS+rXcQMOvZZBosaubslHA2uzAQgH9uvaJhZYJzRsbNkl85DTPpiKN", + "/mLbDvF/yyE2AyKTFYlRvHlsItj/wB/s705u5Xj/g6Z4/4+gjjpJ8l9fKTFH41OW6aTR6wF361pkJMI5", + "3GVA8TVoS0z3QhI6hxTX0nans+4jlqdJug6hyPUjpli/NZKvn2jT0wkOoZjwKbbqt2hwdtk86DoIox6h", + "90SDpdcZTDe9yNRkL+0TzJKOzKyx93IeZUPIzYmSAoPABlxfTuoLbSusvNZ6fh95lB2r+vVVtZaIhKU9", + "1RFmDS1lFsNs/QP9jD/oaB/tcXTFwnK9LhHpgA86cTwbdJToioX4TPIEG7WJRWxyUgOiA543IUH0KpYi", + "vndvNHcfLvrklGWqzXOex/G5+iuIGdXI4lc6Z50dyv9A4B2MgdELRhQh5zyYUj7BNa5BmRlZalrwo0oj", + "4QDOzXpkg7jS+j7rYOpXxd5jHqrbl4fqLtrCmol4AdbLeqS9oMGlBP8IbrIJcBP/ZkuWZvrVYK00IPBY", + "lEMzt1piGQVwuFpudJjfW133A/C+V/twi5Qtx7JPjjGUe9BBs/GgQ0SqzkztDjbouFu3idburGk+pRkb", + "Qric3zivfifwe8U83/ZSp5Wf35VuTVPU141/oW17WWS1Nh+XSao0+LNmXksSgwD5imZ0Ta4rN7KU/4xW", + "P7TCYmV1sdKluY1YQePhi49Kb9PolRbNckoloSSO+GcWFrcNOy5Ck8Tlhte1Eng9S6NVuNg/h1NsZZ2B", + "Y9XqYE2DzZLWRGCnYhzFa94tym20kL0aj9njiAiuQYBgEZW9XxNs3O9OeAuk+d2UbjIHnLp0ZcI9oWlj", + "eiYYvFZyM6UwO8hQFsjE9IybbaDx9C4PeAGupIZ5KdLP41jn/VhlmH+Yiv6Rmm5N+3BNj/ikgOYzI2qS", + "uHYBnUF2LYU3C13D9Xqc6wteT0NLGZAmydCmD7iGuPK9liZJIaAMsLP116j+qNZD78HQLPTKlGgk12Lw", + "4gqFWTq0SMWKu3ROKeP13i1JoGoLtexSepY/lsstEr4F8vJ1tn7pfpslpmGo7rOrb7uut3hldevWUG4W", + "lny0AEb0CkGSfRcEs2y2s25Hg6fPV4Gyxhr4wHdWXzH8ecEY9Wy6iwdrGno8fK5x+CRpNKPpfMhm2mbu", + "QbXAIgSKNFKYszEnusJraNOH+iTphA1NCMpK6e+NSVh3+1G1dOQ0VKe3dzRJ4MornDhPMBqyUOfD0m74", + "Vizq4CQ0I2nY+dLjCdQqdes7mZpPHotHtJ7QKfAP78AV+z6g2D3iz91L/Dm/OtkG86xg4/U5+C4w7x09", + "1NS+NdiKCsY2OVV1sD0aGlI6SwyikOvxTI60dJGXURZMdVoZqd8OMp2sNsRnUKulYtpacpSRmFGJuALY", + "DGSvRNJb1UoFaHJGMpXD9s0BXMyxU3ewSlIxTOHZcci4koRhySCAb1t+o0CSih5WVRPQtZ17WgWN9qQo", + "bnqqWw78TKhH38x71jC+DvsZd6g2l7QZ6g/SByyGmoXa8ySBlETwRmUR6FfdWT2soyTRTbuGyCPdhdsD", + "sYOrb/Oj9NjgmVAiBC9RVhTRU9QavUAG+jelvvSXJpyCR4qx8G0h4/gyDJAIQSzykHCaRRcmzavN+qSW", + "xcgknWgJ00AfnbxFTCA54HORA3gC5GFB3Vd2NToRvrFDq11oDYPt7TaU3sGKgamSvwjO1WRt0qtBvr//", + "7Ftir5onbzvdTpHrab+/3z8AN7GEcZpEncPO8/4+JIBKaDYFanLdigBTT305YVkDMiuNY9cVHwGRIsHf", + "hp3DThzJrKdbUV0YSPtG9bQosue42UaCYxj8125tuyGsXytmBgjfyYSLbrXkNE8SkSplq4oDQFNm4B2i", + "8Bz+/czm+IeiT/yrcFs/J0+0NH8KvxQ+7OeqmU3gHZAC7mDAV8I7gAf0JIZHUC2bI7VKf2oQAZQBHdVx", + "p9spcjwu9FW3IATwCjAHMhyLdObZDR2Nt3Q/Ov5xjY2zXLuRKfqDm5o8UWQjta+dM8yQseR94TJo+geS", + "fra/b6AOTPauaq7Mwy8tR7Ig3ACETEvn76/dzgscla8zO/q9H2lojmaocrC8StVj7sX+8+WV3oh0BBgm", + "ILllPpvRdG4ZHzdZySaqDvBPjmzSmKpEg6oqoX7Vy9WFyHr4KF1J+1BVTFjAaoSCd1nx1FgWLMiPPRMz", + "onWMH0U439ie4jhKhoSv5QNMT6NCVQebpSofAaF5QsukO0g/ZosxunFdAvrarZ9Ve1/g37fhVySsmPlQ", + "I07FOMOYwcKCMSdRWKczLGTprHKCgQwDH10rwnT3nSqdtJVpOgygLqxe+HKpQszl3SAAVePF8homK1mF", + "Yuo7dg2541VmfmLZElqYsOw2EML+TcmX+0lW3c6LgxZT+UlwVqHBgkKuc+blHtpD/78ihU4TBaKuuSMi", + "3PwJ6/GEa3XC3hgHWAvGIyO4jGDIdZvH9x5Ng2l0AYe3X088wgIO1+h7bp1vdFsPSnrre+lDUA4sJZTI", + "YFuEmeSjOJLTZsI8wQJtCFO39UiY95MwLSVshTCTZIlpDl4g45iFRJVtss6pZrZpm0uSjRnmGqxwj1Y3", + "tLotXGlYJmGg8KHsSJv9aYj5RccijsWlmpeTgvRQV/mkip79YzZXFLNB+12S7N56lyQPzXaHTF8XQ2o7", + "OmceSbP3hSaJNqs03595WeY03KGBflocdarD23zQFT6j3tMuSR7CGQf7Djvakpq0z6N+BG4+vYpy4EqN", + "h4R2HrPvbYiN7T/ZnI62eL45w7zmKReIkFVfl/QL1OMJB6dWm7Uun3NODUU686TtaaeKnv0DnRY3d9od", + "2+Hs+swz/r0P7OAryYS6wCr2BxVwyKfAaWyOniUqedH6nqacxkuifeXCgmZgc8WkwNpanGkPAvSmUsQ6", + "F3lKxCXXFQfc1HSdu0mSp4mQTDa+nGHtnnVA3+YbmvVZhz539JhmfZvdsfgIvVzi7r+yVQjsRuh+74vp", + "7m34dS8QMuuNjNfgghNfyAygJ6R2EiyY4o1IKxOJmITolZQZt1kbjcudhgBiNYzGEDuTkXM2HrMCR/Ac", + "EAybrsbOuNvoq8WUr6u0Np5/xbzann9FjdGcjCNqReAcfdwWn4YGgPqTqgnB9Wf/+Hj6aoMHopDZj2p4", + "bc7D7np621aPURx/JO/1OXq9G0KFtzcjgJZ6j5gOo5owaTwSNbtHN83uZ1s9fA2F7vjcNcPwHrnmx3tw", + "2lqy285Bi0FU7kHqvfmaYlu9+OpONmXdhbtu5f7rWHwfb8H2Frx04auXYFNhNIewrnZX4M9sDvbecNQD", + "BOeN3YH1aHZ/BcaBPLgrcCEcfALK/HrmnLELrpCw+9u8O5YDyHd1a9RT9d4XdWDrPbkpYq7RhYTRcCip", + "Wx/+WfO/9HpVOv21UbdM04++lZvVz3Gq7Xe/69c8Jiy7PTu6vxMJ8ECee1agFO3uWHVjlCzdLbFsy5lx", + "reNqN8T66NzY4NwIy7LRs3BPm+Ubb22u7OyZwvdWhiKyUjNpukhPD0yk2gccnWZjTfkauvr5Lglq23LW", + "gy62W5G7Cm0/SuBF7uXrscQq4niPJknPwMStwkk9W/EesVQDSupu2KmGw+f1r/IDsj5yUxtuokmyBY5C", + "lNi9YMqCzyLPelKjy7dwhPikAV6PdV1yinXPnphcHqEIZB97gFQeOvWEtN09HXAv6iH2IQmtNY6g4yKO", + "WQBIKiabxYxlUxGWcT1T9LbQ80c7sp6f9tfAxL2DjmRZngw6ZCZC1tVoWboTabvATCpywC+jbKqGFExp", + "OjFJQex+RbMZCyOasXiOXeqGWFgdrE1cYeCsxnmWp+Xkomb7YVneiJRMhVRNmRU0E5JdkrIwSlngGvo1", + "lJs1O3/8/VcNlcVmIxaGLHTq5xLBf4I4YjwbShakmDAi4lEW0Tj6i2lM3/7/wbrNRZ4OuCM6ljivsLSH", + "xNCrktv9EMsVzQLXSltF9YQ1Fe/WOHqUJAvHJvM486pEUFxX9VW6WxbVG5TpWmY2CMytSPREpBmN28tz", + "MzYjxk6gvhkiiJ+Pko3zWEkEK2pKkk9Lm4aWMmFwcrIpi9IBL0tD2SWYXAZ/rmG4Uh4SGgTqTyyAKKiM", + "TCOZiXTeH/D3PJ5rWSeVqKvBk1fxciNpsMozQSiRFrBc9VYcHa3FWnnN779QM+9xMO1bKdr8I2wl4Jqr", + "Poq5VmLOsh2yBZGblHagey2PSDAvfVgapJjJrehKhm+kKYKZFmnKinzKLCRUEhYB5t84phkZMwaJugAC", + "rIe5t0wXTaENWlKYcW/Kz2OrIqXBj0Sv1EJnhnXDBOGzRmUbasRIcPyEHywyu/PDbXHycEjM8eTQazWa", + "m8yAy/w3z7UzBxY/+yTYn2f/0AvUxYza5xv07cDxtXTmLE/89VWiDuVxHscEof/Qc89m1Q1t/tIKSegM", + "ZQ4lpIzGwyyasSHw1Pkh0a0Dl8Igv1EUR+Me5GCHUk2oZAyqltZhFdxJvSY4AF9O5jJBbNXjBYby6Kq6", + "xFW1IuaXnTCb8Fet9LlcRWwU/HdRKTQXRzWlHTv6wBi8yhz8cn+cfDSlbYS426lYkMFP7tHw/3KZgeFu", + "AbiLyQRYlDZhHsZRc0Y/M4I5UJ1SEu9ehSY24LqlEY0pD6rqRC5ZL6CSSX10YPaBQKTqPorqpZ8RodFe", + "0fG94kSY3JGd2455sjKahqtWqcw9v1N50RTrLHPzzK2ZbDFKAKlwpBcloDjpkNNMyzfLZl1/Kn3UI0uz", + "UJc6wM+OJGabyOgsAWGjU4jKIhMK5KCGUm19ym2LrdXxVzRjH6IZ8145NqDu/8QyZLofcf679uYuCQs9", + "JtksKkyJBwPRUDJOlCj35qUEpvddYmypZgL2mUFQNOjmblwybCSWxtz33eneBHA2ssFP0N/OIzGKsTxe", + "TpdcTl06uYmbKeJwu932yVHpc2HalIB5F8cCqUCdh0nKEhqFRguvqOf9Jeo1tH//NGug9NugVMNAmg9J", + "+PlBqtJVmt/VGbn3JSh2Yin8VplN/Wr17hir4XnBnd9dcGJuzzgPR7W8hZyyJ1mWxUxdg/dMzvNme5P2", + "koNkZdqPoKhv3lrEWJ1vprF4TsY5D1lY5jrt2YAXQcbDREQc3J7knAfTVPDor0o/meq53Lb98TLKpgMO", + "6aQB6oxIgU+KKbtgPFeaYSAmPEKoIG7HohMxRnGUzSH5M7w5XiXgONYILO4Kh54ZS69YiHsqMLbhURuW", + "D3uTU//UruWO3dXbS7EHhNdrfGVd5dayZE0e3Lyky1LKJQW7dLvbs1vBwqA4jrHGeYFyEs1mOQysS0BM", + "iVhMooDGIGFSyL2oG52JC1gGeVgWgHLAdU58mc+Kb/vkgzsK9IUorrdKkKWSVToFwIUBH80NnMNiC0Bp", + "YW6bHeA4T6VIV7UElLbuxuwB7lbdDquAM6JWdgFc7YdrGaiwwg1JKPDMQOnYQ7fKdgg1PU/F+xP36MKd", + "vC4megTzNETcFN8Cm1rxKkOHHTJmNMtThl6s6L6Ka/dgXDMcsiGWbOq07ix63RVQnyxyL6NXPUgpuzA+", + "V6QTyqO/4Muertsrqm6RjN47PevHJJ3M1vvAsaD4Q7mSuntlAaFs5mAvqZhXuvZW0wUBgy1oZVuXjgW7", + "v6MrxwbJ917H+PmuItsn5LJMxMzEy9KKTBgEjWHhBs3c5Efu6SZX9VBeUWvGXtoiounSozmROl10SxxU", + "XfzsE7jRGiXmYHNq+GsY2VoOtOBNvWwd2kHyZVEdis9se+FWnYm0gOb7Y8o4EbMoy1jYNcNQlz1pkfuh", + "WYDVOydPZkIqRg7UCT6OUpk97RNog0INteIsDkkkSZKKi0hdM02gJNXof10SIcyfdLD6+uQoSZh29XWx", + "Agc8E3rOpmyX6GhRhAM0oIKmnNPoDXuFb1Mpfav3ESjtHl+p6tqjTdluRZKRnjZ/OrLfSvmKcT3BhgLt", + "Kpoa0SyYEjE2fFBIFUVtx7HIcfmlhptsDHhFpvMI03YKBOStxzp/W41KzIrAMGExG9rtwVw9rbfyka90", + "U3WOr3Rb74Ty+ftx43HS1MtmBnfWSo96VqeYD1NmCGNKLxgZMcaLUxUiE1P1rY4tVLcbCBrSDwgil/H8", + "7rAe8scqzFfRScxtd7FWQuPY3osbVBLb0BZRce3V/Jon8Gc2P3czwDRnP+uRc22if0TJdRTC5RtRVglt", + "+dEcaXUYhW21QlNeh1cN8v3950EUwr9sc7rhGxziru2ydhgPCjbXER71O9cb8+PZOg5WuulGZyj9+1Zx", + "dvUMduSUpHv3UY/+6e4H3hS7uCL9+E/DvS/6rxrWrg/ItqCyptziGpO3GOVyI7wdwCMi71YQedemmO4i", + "H7EldDBh2W0hgv2blC+P4At1M/o1CDBRt8JGP6sqFeq8GFk8J4LHmOMx51E2hHQaaAky4X+o6zb6MO2O", + "drdl0V/naL5R1nl4HkJbPMshW9oeXhYa3RXhLoSPw4pBxHiRFgltGb6ALEf3gjlgtWEhHM64AU44FjLT", + "3TYB/3yAayYMikypJDIPAsZCJbXuLWcgSRqxrqnsWtwxERcs5ZQHrB07mL41rBe6u8UR8keB22gsJNY3", + "N2VSxBdMEkaDafHaEIWMZ9E4QiSywnEODHRpgfoz4LpD7SVsECg1AbDQXh27JIlzxxRTDsMbcNdpF4Y5", + "fMVkNOFochkxEmBmbcEVu0dXIHLHKZNTAs98FzQ2HiHaTmF2jURywFUZcNczjQVTFvaHDfKiWP1Gv5z1", + "XvC2JA5+suOtyoSbPC1ro2g2i9xNIeHh+YJSFrniFCvT9DCdpNEFzVjLl+o4nsFZtqfaTaNwmX04YWlP", + "nWwyoQEjSRoFjNiqDQZj00ev6MN/cl7ftvfrr+/UwXKixnVXk2HC4B+YWfDXX99pHcwhkTr1q2Jqf9cz", + "EC6i3UarYY14t2Q/1JT7XveCg75pA6LLPl5igzW78zbEGrWtTmzLpOjeF6CvtlbF1UhTGxl9pLn8QqLH", + "9Whs3IqxcXukBfu25HSexGJE42IQWKdPTIAKfsZs3pZUCcgLpaqPCeXzZYe4HkeN1LzPh3oAm3u8W+eA", + "97wv64XYUMpVfdlJ++aLmQhZrD5VXpgradMr32XiQb47P+pft0j/sty9SaFVPgwXPKoYN67KeMhoTt6+", + "KsQYxP3CD42SbMC9omzCqpJst2fm/o2pbg/RmKaIqkxJ1yVsQyaLDmEss8ife6uuU1hjC45TdDJJ2QQG", + "UPKkOmpypDp69KOyitCyXSl7UenSa+UZh7q9z2y+OaUL2GTngawwigd2LltxUfLyLGcVb7B2QNVGswb8", + "ulVXKBjpjhyhoG8fVaDAv/MGDLN7NarwnVZ7X+DftqaIBrrRNgfT83KdSXf6aGfYip2hkQIWOi1BLa1N", + "e3XjW7C9+zclBR5IEO8CStGxtw2+RQ2CQDsI7YZStuUctPphdWNk+vDcgpootuXtrDjv2jk8GLE4FqnO", + "YALY9iwj50dBwJLskFQ395w8cW4tT9UVZIIGjCzNgyxPWUj+efr+N1e/LzWYsatsL5AX56pqKC55LCgq", + "+ZLOGGQUVVcjSo5P/0UgK5rMI5i4GuaAyyRlNJRTxjKdQlMVDEScz7jsqtsF3H669kp3Pk7FrEsy0SUm", + "+rZ7Rj4Zb4xhFHata8bwM5s7nxQbd88IhmOE0YxxyF3X7/cxMqOLmWOKu55u/1yPR13UGEa0olvi5ZRx", + "p1QkzW0ItusbOeDnk1TkyXA0Hxb9neM8s2nKGDm3o/tv0w2GyZqOMjFhkBpK9Tjg2KUzW0+3xN9rg2PH", + "fZF/Xv+vGxd/ZU+wbsfwh6rMrugsibHjn9QOYRh0yZGo2DDouDgAl5fvdoB81c1YsUgmui5TlHiizBJg", + "5O9m84R1oYUBf7b/7Hlv/6C3f/Bhf/8Q/vtPt/LlAXy5f/DTty//8/eXL4/e/HH0y8+vD5799u/94//9", + "/s3PXRrMWC/iQfcomDHylgf97iTJei96WZ6ORDfiSZ51D57Vejvw9fZsI70926/19szX2/Nybz8+//d/", + "Dn75/ej7P777199PTp+96k5iMWJX3Z/gH3Is0qTUm8gz1d0LdY78JgiwY280b9zdhjL1HV15f1Zb39XW", + "5wWeepY5dDiszNKITx5Nt65P1NqaQBJT3iLSFYo1WGqxiS0aaqGDTT1BWnvtBUtl1UK7ONb1llhIlyzH", + "5qyZJ6qjXRsz1SAemC3TMFT9AeYkFWEeZOSYZjQWk/W8uzi7hC4aTZ7qx61aPNWe7haOXo3AS0Ax5Xff", + "6qk3cD368ZwPe1/UP61dtNQaLo761ANs8Z4M/T6aRrdiGr0WmSw0ny4igQnLdr//+zcqUB5jPevW1msS", + "32KL7CL602bZXZDgNoyykqXZyufpzZL/vUZRXJ8LNLVu7bDeo2G4FLidhmEPYNKlFEEEag+4itEGFdHe", + "+Hq69ZtjodvoARlTfqTW4TGN2GIgZUNmY5FeW/L7E/iGISQDg34wgHLpHQdJ+O4fAsWlCmhxRzcr23/T", + "OQA/PqTsXkCTliC3LefxMyzykpva72wmLpjDLuNUzBoZxrmy3TjDdBvb1vN8vBlulmQNaTh0sbVboiU/", + "o3tEqkuIovdTork5PgQy3L9pufxQksf5SW6bt9GV6dy5od5TUt/mRXh1HejGee3BpRXYBtctVonSYBpd", + "sGaPpiMsYGxF+jGwzou6oYdmsXxAvnSGElw62A5RJvkojuS0mShPsMBSotQNPRLlvSVKQwnbIMpUjKN4", + "WYD8CPeDmNINlkBdrGcb3YgbyE0QGg74gXkWVHfVS1J6hcgpy7KIT1rFUHF2WW0cnIN/rHRI1B5S7bIi", + "dQegC5jaOqVnlopYkohfiChgAz5hXNNenxzxcpqigHLMazDL4yxKYlabJgnZOOIs7JOjAa/8SCJJ4oh/", + "xvhIJ7KaJkmffJhGsqS2RJIwYK5ITlk44GGemjQblYa/kWjrMpmSUzajEZdF4tNG22SFqbbqilFmhx07", + "Zej5etivXOLue2h4OaYtN/rl+d6XqKWDho9R3/N4TmQeTOvMo4FwQ20Fg3TghWMdF1kpMFlXMz8lEdd4", + "gpQ7rsW5VF3YjxaYQFUD13dg6HHEaayW3QgC2WSdrLPMcqUoerQebsevhK5L2At9SqpU6zUP3hY62N+d", + "bHwohrz1iWyx78hSOtPmuR2S2rbMZ9dQBnZI8A8v7o9uRnOQ+cgu65L82eWiW/T1L3W0KZ9/6kCJVdz7", + "6YNGFPPGF5S2YIPhBKduu7sOK3AH8+i8s9h5p8r8dVFT2tqSvcB7wXUb3NLtVgs/d2C7QYr1DMRHWu7v", + "99xHRlX4fnmFY8HHcRRk/gt0hYSWk+SCo2/vi/uxDL5Xv2JUel6u9JUbvwN3jZVo9YFcN7ZKb608dZXu", + "geUw/4rbRoN13i2yir/uZim2u7Z+eF9SYLu08eguvKLGQSzhLmW6pW9frdiw/L3rULmWL9tiPq2eKCs4", + "/GycS5f0cQd83WqMtuz4evR9S1c+2q7LZQHlAYubvSCO4Xe8bJeYh/wRxbHaLHX9jrjirWDKwhysHIFW", + "F0k0VjVTRmjKBlwAtk3ZqqBr6ft4RtOM0LGaKCQ4g95xdbJo5nulgxK3Qg+8mXsT7tduzIAr6aL32xJ4", + "zXsT7OKW9dhgSvlkgdfdcSwkk4SSNOdccW35oOchsqPU76OCQ4onkYKRLBOY/sy80Gt3gGOdp1OyEJHJ", + "kklKQya7ABdm/lZtgzMNDtHjz4I/PCC2xr3aPVvjQO5f6rKbZnBYxi0zeM7N4dlzDspmhv9oy9fPc/eo", + "9byy+Xt6NLg8HnLNPFCQWyOlrcETGb3qBaKc8c9jbSmKbed97C0P4jx0HHHoFYH+fBBQbUwnETY41A12", + "PFhrIyFiRvnNmks+0KtjET4030y7nV4K/UCvmvJYNkZIe99eDJVu1atQ7+Bu3Qn1ILwKBf509/0IDdFc", + "k2Ya5N3elwwXqhZQ7HXCc0hr+SFtW350wtuKE96GKKPb/BB2W7Z7fweC44FYDDdGRNrlrupIJ1m6Uzra", + "liPdOuffLsj4EZmpAZkJlmVTh6tqm6UX/vyfv4qAxp1uJ0/jzmFnmmXJ4d5erL6cCpkdfklEmn3do0m0", + "d/EcAJvTSLUt8c6d6js3uKR1Djvffffdd7DhtdBCjBPDF/sJ3oOKLuXh3t4X/P5rnyZR/7Pgk+mf/UDM", + "PN3qBkod50ptZTyfqVXCD3mn26HqfzOGTmRnvoEVK3ocizysDcuqJf1A/W5WQvGv3pTag1yRH55d0DhH", + "W74Y23ADSTJBgikLPqtrU5SSMaNZnpp08v1C2njTyXvm4IQj9WJ2wWLrJRgIPo4meWqtHLWWX2FJ2Wnc", + "NBJgcB+ZUU4nTCJAbNeAKqFxE2fivO3I2uNOb0QlC43vqHcw1XDC+phsmsCQZlQ1SDCpa8QnhIt0pgM2", + "kjQK1FeQoEENJKZ8kquLGqDBS0KDVEhJTEZY2SeYqhaSE8g5D1iI+CM2qotdIaMRKfIUSvKQ0DwTPVjk", + "dMZCzJiQTdmc0EnKmHeONouhxwMSCUGSlCUpk4xDPIveg4SOojjKIibJiAafESwfT6uuzq9pXEUTlvZy", + "HmW4UstpwPTrGdIHe8tXC2O8SAMaB3msbwAMt9qSt7cLJa/qrR/nacp4EOnVhOGqTW3TYlHX07BxPrYx", + "foZ2PZFy2LcJ8WnRc82zud7/UZJIwjgkApmLXK2Zoh9FMWqe0Hz0FyuFHUJOE3Ip0s/jWFxCVkEl3Cdq", + "4/gEt7ggwrnM2AyHrqQ75o+GbgPKgS5nCP8REsanII7mIi8CHVkgsA3Vj0R3ZnjIdAkNIrKoBIaYpoJH", + "f6kiOFBgLRhUNo3SsJfQNJsr2ZCNRTqTXUsk8HiiyKRLTBylnnHI4uiCQQSjWf4umVIe4vbQ+UyxQCDi", + "mMHZgaIG31RN/EPKYormKPnZv11qUTxb9JpnURYz1UWFuDH8U4tj9c3Y8OVy2nBb9TmZl16C3V6zlAaf", + "9dKKMe6VYX4lSHGP+2VboQmOi3gYXURhTmOpCrvxqRIj5lRBLYxHzOCBIflArFt9st7plS2VPoY2Z9w6", + "cytq3/S8bM+eOUEJYJmLGq20ndnromqSCjUkFhJq2ErkMp4rPlTSyYh0KfAkmdE5hDKq5ZjNWBjRjMVz", + "Qi9oFJtEQ5gap3yq2mFj300TkzYkYCouIVBSp49lZr7V8GjKaTzPokCSJE8TIZXg0U3pbTMnjkHztGeo", + "k5pWzXMqQtwqyA6iRGmfvDNlZ+UmtalMDcamX4IBEsjsgsJWDXEcs6toZBqAQyBgnKaRkNXVkZ2vZ1//", + "fwAAAP//zw/7OHJQBAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/apps/list_app.go b/api/v3/handlers/apps/list_app.go index d77c5d9cab..199682bae8 100644 --- a/api/v3/handlers/apps/list_app.go +++ b/api/v3/handlers/apps/list_app.go @@ -9,6 +9,8 @@ import ( api "github.com/openmeterio/openmeter/api/v3" "github.com/openmeterio/openmeter/api/v3/apierrors" + "github.com/openmeterio/openmeter/api/v3/filters" + "github.com/openmeterio/openmeter/api/v3/request" "github.com/openmeterio/openmeter/api/v3/response" "github.com/openmeterio/openmeter/openmeter/app" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" @@ -52,10 +54,61 @@ func (h *handler) ListApps() ListAppsHandler { }) } - return ListAppsRequest{ + req := ListAppsRequest{ Namespace: namespace, Page: page, - }, nil + } + + if params.Filter != nil { + id, err := filters.FromAPIFilterULID(params.Filter.Id) + if err != nil { + return ListAppsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[id]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.ID = id + + name, err := filters.FromAPIFilterString(params.Filter.Name) + if err != nil { + return ListAppsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[name]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.Name = name + + appType, err := filters.FromAPIFilterStringExact(params.Filter.Type) + if err != nil { + return ListAppsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[type]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.Type = appType + + status, err := filters.FromAPIFilterStringExact(params.Filter.Status) + if err != nil { + return ListAppsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + {Field: "filter[status]", Reason: err.Error(), Source: apierrors.InvalidParamSourceQuery}, + }) + } + req.Status = status + } + + if params.Sort != nil { + sort, err := request.ParseSortBy(*params.Sort) + if err != nil { + return ListAppsRequest{}, apierrors.NewBadRequestError(ctx, err, apierrors.InvalidParameters{ + apierrors.InvalidParameter{ + Field: "sort", + Reason: err.Error(), + Source: apierrors.InvalidParamSourceQuery, + }, + }) + } + req.OrderBy = app.AppOrderBy(sort.Field) + req.Order = sort.Order.ToSortxOrder() + } + + return req, nil }, func(ctx context.Context, request ListAppsRequest) (ListAppsResponse, error) { result, err := h.appService.ListApps(ctx, request) diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 412dff5f4d..e1c26941c1 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -285,6 +285,31 @@ paths: description: List installed apps. parameters: - $ref: '#/components/parameters/PagePaginationQuery' + - name: sort + in: query + required: false + description: |- + Sort apps returned in the response. Supported sort attributes are: + + - `id` + - `created_at` (default) + + The `asc` suffix is optional as the default sort order is ascending. The `desc` + suffix is used to specify a descending order. + schema: + $ref: '#/components/schemas/SortQuery' + explode: false + style: form + - name: filter + in: query + required: false + description: |- + Filter apps returned in the response. + + To filter apps by name add the following query param: filter[name]=my-app + schema: + $ref: '#/components/schemas/ListAppsParamsFilter' + style: deepObject responses: '200': description: Page paginated response. @@ -8769,6 +8794,19 @@ components: $ref: '#/components/schemas/StringFieldFilterExact' additionalProperties: false description: Filter options for listing add-ons. + ListAppsParamsFilter: + type: object + properties: + id: + $ref: '#/components/schemas/ULIDFieldFilter' + name: + $ref: '#/components/schemas/StringFieldFilter' + type: + $ref: '#/components/schemas/StringFieldFilterExact' + status: + $ref: '#/components/schemas/StringFieldFilterExact' + additionalProperties: false + description: Filter options for listing apps. ListChargesParamsFilter: type: object properties: diff --git a/openmeter/app/adapter/app.go b/openmeter/app/adapter/app.go index 9e2ce97430..0122586efd 100644 --- a/openmeter/app/adapter/app.go +++ b/openmeter/app/adapter/app.go @@ -11,10 +11,12 @@ import ( "github.com/openmeterio/openmeter/openmeter/ent/db" appdb "github.com/openmeterio/openmeter/openmeter/ent/db/app" appcustomerdb "github.com/openmeterio/openmeter/openmeter/ent/db/appcustomer" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" ) var _ app.AppAdapter = (*adapter)(nil) @@ -81,10 +83,6 @@ func (a *adapter) ListApps(ctx context.Context, params app.ListAppInput) (pagina Query(). Where(appdb.Namespace(params.Namespace)) - if params.Type != nil { - query = query.Where(appdb.Type(*params.Type)) - } - // Do not return deleted apps by default if !params.IncludeDeleted { query = query.Where(appdb.DeletedAtIsNil()) @@ -107,6 +105,26 @@ func (a *adapter) ListApps(ctx context.Context, params app.ListAppInput) (pagina query = query.Where(appdb.IDIn(appIDs...)) } + // Apply API filters + query = filter.ApplyToQuery(query, params.ID, appdb.FieldID) + query = filter.ApplyToQuery(query, params.Name, appdb.FieldName) + query = filter.ApplyToQuery(query, params.Type, appdb.FieldType) + query = filter.ApplyToQuery(query, params.Status, appdb.FieldStatus) + + // Ordering + order := entutils.GetOrdering(sortx.OrderDefault) + if !params.Order.IsDefaultValue() { + order = entutils.GetOrdering(params.Order) + } + switch params.OrderBy { + case app.AppOrderByID: + query = query.Order(appdb.ByID(order...)) + case app.AppOrderByCreatedAt: + fallthrough + default: + query = query.Order(appdb.ByCreatedAt(order...)) + } + response := pagination.Result[app.App]{ Page: params.Page, } diff --git a/openmeter/app/adapter/customer.go b/openmeter/app/adapter/customer.go index 573c825d0a..c5fdf05cce 100644 --- a/openmeter/app/adapter/customer.go +++ b/openmeter/app/adapter/customer.go @@ -6,10 +6,12 @@ import ( "time" "entgo.io/ent/dialect/sql" + "github.com/samber/lo" "github.com/openmeterio/openmeter/openmeter/app" "github.com/openmeterio/openmeter/openmeter/ent/db" appcustomerdb "github.com/openmeterio/openmeter/openmeter/ent/db/appcustomer" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/entutils" "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" @@ -30,7 +32,9 @@ func (a *adapter) ListCustomerData(ctx context.Context, input app.ListCustomerIn Page: input.Page, Namespace: input.CustomerID.Namespace, CustomerID: &input.CustomerID, - Type: input.Type, + } + if input.Type != nil { + listInput.Type = &filter.FilterString{Eq: lo.ToPtr(string(*input.Type))} } if input.AppID != nil { diff --git a/openmeter/app/app.go b/openmeter/app/app.go index 876f5bc38d..ecbc9c9684 100644 --- a/openmeter/app/app.go +++ b/openmeter/app/app.go @@ -4,10 +4,13 @@ import ( "context" "errors" "fmt" + "slices" "github.com/openmeterio/openmeter/openmeter/customer" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" ) // App represents an installed app @@ -145,16 +148,46 @@ func (i CreateAppInput) Validate() error { return nil } +// AppOrderBy represents the field to sort apps by +type AppOrderBy string + +const ( + AppOrderByID AppOrderBy = "id" + AppOrderByCreatedAt AppOrderBy = "created_at" + AppOrderByDefault AppOrderBy = AppOrderByCreatedAt +) + +func (o AppOrderBy) Values() []AppOrderBy { + return []AppOrderBy{AppOrderByID, AppOrderByCreatedAt} +} + +func (o AppOrderBy) Validate() error { + if !slices.Contains(o.Values(), o) { + return fmt.Errorf("invalid order by value: %s", o) + } + + return nil +} + // ListAppInput is the input for listing installed apps type ListAppInput struct { Namespace string pagination.Page + // Sort + OrderBy AppOrderBy + Order sortx.Order + + // Internal-only narrowing AppIDs []AppID - Type *AppType IncludeDeleted bool - // Only list apps that has data for the given customer - CustomerID *customer.CustomerID + CustomerID *customer.CustomerID + + // API filters + ID *filter.FilterULID + Name *filter.FilterString + Type *filter.FilterString + Status *filter.FilterString } func (i ListAppInput) Validate() error { @@ -196,7 +229,37 @@ func (i ListAppInput) Validate() error { } } - return errors.Join(errs...) + if i.OrderBy != "" { + if err := i.OrderBy.Validate(); err != nil { + errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid order by: %w", err))) + } + } + + if i.ID != nil { + if err := i.ID.Validate(); err != nil { + errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid id filter: %w", err))) + } + } + + if i.Name != nil { + if err := i.Name.Validate(); err != nil { + errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid name filter: %w", err))) + } + } + + if i.Type != nil { + if err := i.Type.Validate(); err != nil { + errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid type filter: %w", err))) + } + } + + if i.Status != nil { + if err := i.Status.Validate(); err != nil { + errs = append(errs, models.NewGenericValidationError(fmt.Errorf("invalid status filter: %w", err))) + } + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) } // UpdateAppStatusInput is the input for updating an app status diff --git a/openmeter/app/sandbox/helpers.go b/openmeter/app/sandbox/helpers.go index d59a33707b..1106ca15ad 100644 --- a/openmeter/app/sandbox/helpers.go +++ b/openmeter/app/sandbox/helpers.go @@ -8,6 +8,7 @@ import ( "github.com/samber/lo" "github.com/openmeterio/openmeter/openmeter/app" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/models" ) @@ -40,7 +41,7 @@ func AutoProvision(ctx context.Context, input AutoProvisionInput) (app.App, erro // Get the sandbox app list sandboxAppList, err := input.AppService.ListApps(ctx, app.ListAppInput{ Namespace: input.Namespace, - Type: lo.ToPtr(app.AppTypeSandbox), + Type: &filter.FilterString{Eq: lo.ToPtr(string(app.AppTypeSandbox))}, }) if err != nil { return nil, fmt.Errorf("cannot list apps: %w", err) diff --git a/openmeter/app/service/list_test.go b/openmeter/app/service/list_test.go new file mode 100644 index 0000000000..644916d6a7 --- /dev/null +++ b/openmeter/app/service/list_test.go @@ -0,0 +1,273 @@ +package appservice_test + +import ( + "context" + "testing" + "time" + + "github.com/oklog/ulid/v2" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openmeterio/openmeter/openmeter/app" + appadapter "github.com/openmeterio/openmeter/openmeter/app/adapter" + appservice "github.com/openmeterio/openmeter/openmeter/app/service" + "github.com/openmeterio/openmeter/openmeter/testutils" + "github.com/openmeterio/openmeter/openmeter/watermill/eventbus" + "github.com/openmeterio/openmeter/pkg/filter" + "github.com/openmeterio/openmeter/pkg/pagination" + "github.com/openmeterio/openmeter/pkg/sortx" +) + +// minimalApp satisfies app.App without billing or customer operations. +type minimalApp struct { + app.AppBase +} + +func (m *minimalApp) GetEventAppData() (app.EventAppData, error) { return app.EventAppData{}, nil } + +func (m *minimalApp) UpdateAppConfig(_ context.Context, _ app.AppConfigUpdate) error { return nil } + +func (m *minimalApp) GetCustomerData(_ context.Context, _ app.GetAppInstanceCustomerDataInput) (app.CustomerData, error) { + return nil, nil +} + +func (m *minimalApp) UpsertCustomerData(_ context.Context, _ app.UpsertAppInstanceCustomerDataInput) error { + return nil +} + +func (m *minimalApp) DeleteCustomerData(_ context.Context, _ app.DeleteAppInstanceCustomerDataInput) error { + return nil +} + +// minimalFactory satisfies app.AppFactory for testing without billing wiring. +type minimalFactory struct{} + +func (f *minimalFactory) NewApp(_ context.Context, base app.AppBase) (app.App, error) { + return &minimalApp{AppBase: base}, nil +} + +func (f *minimalFactory) UninstallApp(_ context.Context, _ app.UninstallAppInput) error { + return nil +} + +func newListTestService(t *testing.T) app.Service { + t.Helper() + + db := testutils.InitPostgresDB(t) + t.Cleanup(func() { db.Close(t) }) + + client := db.EntDriver.Client() + require.NoError(t, client.Schema.Create(t.Context())) + + adapter, err := appadapter.New(appadapter.Config{Client: client}) + require.NoError(t, err) + + svc, err := appservice.New(appservice.Config{ + Adapter: adapter, + Publisher: eventbus.NewMock(t), + }) + require.NoError(t, err) + + for _, l := range []app.MarketplaceListing{ + {Type: app.AppTypeSandbox, Name: "Sandbox", Description: "test sandbox"}, + {Type: app.AppTypeCustomInvoicing, Name: "Custom Invoicing", Description: "test custom invoicing"}, + } { + require.NoError(t, svc.RegisterMarketplaceListing(app.RegistryItem{ + Listing: l, + Factory: &minimalFactory{}, + })) + } + + return svc +} + +func newNS() string { return ulid.Make().String() } + +func makeApp(t *testing.T, svc app.Service, ns, name string, appType app.AppType) app.AppBase { + t.Helper() + base, err := svc.CreateApp(t.Context(), app.CreateAppInput{ + Namespace: ns, Name: name, Type: appType, + }) + require.NoError(t, err) + return base +} + +func TestListApps_FilterByID(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + a1 := makeApp(t, svc, ns, "App One", app.AppTypeSandbox) + _ = makeApp(t, svc, ns, "App Two", app.AppTypeSandbox) + + a1ID := a1.GetID().ID + result, err := svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + ID: &filter.FilterULID{FilterString: filter.FilterString{Eq: &a1ID}}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + require.Equal(t, a1.GetID().ID, result.Items[0].GetID().ID) + + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + ID: &filter.FilterULID{FilterString: filter.FilterString{In: &[]string{a1.GetID().ID}}}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) +} + +func TestListApps_FilterByName(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + _ = makeApp(t, svc, ns, "Billing App", app.AppTypeSandbox) + _ = makeApp(t, svc, ns, "Payment App", app.AppTypeSandbox) + _ = makeApp(t, svc, ns, "Other App", app.AppTypeSandbox) + + result, err := svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + Name: &filter.FilterString{Eq: lo.ToPtr("Billing App")}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + require.Equal(t, "Billing App", result.Items[0].GetName()) + + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + Name: &filter.FilterString{Contains: lo.ToPtr("app")}, + }) + require.NoError(t, err) + require.Equal(t, 3, result.TotalCount) +} + +func TestListApps_FilterByType(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + _ = makeApp(t, svc, ns, "Sandbox App", app.AppTypeSandbox) + ci := makeApp(t, svc, ns, "Custom Invoicing App", app.AppTypeCustomInvoicing) + + result, err := svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + Type: &filter.FilterString{Eq: lo.ToPtr(string(app.AppTypeCustomInvoicing))}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + require.Equal(t, ci.GetID().ID, result.Items[0].GetID().ID) + + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + Type: &filter.FilterString{Eq: lo.ToPtr(string(app.AppTypeSandbox))}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) +} + +func TestListApps_FilterByStatus(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + a1 := makeApp(t, svc, ns, "Ready App", app.AppTypeSandbox) + a2 := makeApp(t, svc, ns, "Unauthorized App", app.AppTypeSandbox) + + require.NoError(t, svc.UpdateAppStatus(t.Context(), app.UpdateAppStatusInput{ + ID: a2.GetID(), + Status: app.AppStatusUnauthorized, + })) + + result, err := svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + Status: &filter.FilterString{Eq: lo.ToPtr(string(app.AppStatusReady))}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + require.Equal(t, a1.GetID().ID, result.Items[0].GetID().ID) + + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + Status: &filter.FilterString{Eq: lo.ToPtr(string(app.AppStatusUnauthorized))}, + }) + require.NoError(t, err) + require.Equal(t, 1, result.TotalCount) + require.Equal(t, a2.GetID().ID, result.Items[0].GetID().ID) +} + +func TestListApps_SortByIDDesc(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + a1 := makeApp(t, svc, ns, "App Alpha", app.AppTypeSandbox) + a2 := makeApp(t, svc, ns, "App Beta", app.AppTypeSandbox) + + var result pagination.Result[app.App] + assert.Eventually(t, func() bool { + var err error + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + OrderBy: app.AppOrderByID, + Order: sortx.OrderDesc, + }) + return err == nil && result.TotalCount == 2 && + result.Items[0].GetID().ID == a2.GetID().ID + }, time.Second, time.Millisecond) + require.Equal(t, 2, result.TotalCount) + require.Equal(t, a2.GetID().ID, result.Items[0].GetID().ID) + require.Equal(t, a1.GetID().ID, result.Items[1].GetID().ID) +} + +func TestListApps_SortByCreatedAtDesc(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + a1 := makeApp(t, svc, ns, "App First", app.AppTypeSandbox) + a2 := makeApp(t, svc, ns, "App Second", app.AppTypeSandbox) + + var result pagination.Result[app.App] + assert.Eventually(t, func() bool { + var err error + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + OrderBy: app.AppOrderByCreatedAt, + Order: sortx.OrderDesc, + }) + return err == nil && result.TotalCount == 2 && + result.Items[0].GetID().ID == a2.GetID().ID + }, time.Second, time.Millisecond) + require.Equal(t, 2, result.TotalCount) + require.Equal(t, a2.GetID().ID, result.Items[0].GetID().ID) + require.Equal(t, a1.GetID().ID, result.Items[1].GetID().ID) +} + +func TestListApps_DefaultSortCreatedAtAsc(t *testing.T) { + svc := newListTestService(t) + ns := newNS() + + a1 := makeApp(t, svc, ns, "App Oldest", app.AppTypeSandbox) + a2 := makeApp(t, svc, ns, "App Newest", app.AppTypeSandbox) + + var result pagination.Result[app.App] + assert.Eventually(t, func() bool { + var err error + result, err = svc.ListApps(t.Context(), app.ListAppInput{ + Namespace: ns, + Page: pagination.NewPage(1, 20), + }) + return err == nil && result.TotalCount == 2 && + result.Items[0].GetID().ID == a1.GetID().ID + }, time.Second, time.Millisecond) + require.Equal(t, 2, result.TotalCount) + require.Equal(t, a1.GetID().ID, result.Items[0].GetID().ID) + require.Equal(t, a2.GetID().ID, result.Items[1].GetID().ID) +} diff --git a/openmeter/billing/service/profile.go b/openmeter/billing/service/profile.go index 3dadf2132c..a20abea7e1 100644 --- a/openmeter/billing/service/profile.go +++ b/openmeter/billing/service/profile.go @@ -12,6 +12,7 @@ import ( "github.com/openmeterio/openmeter/openmeter/billing" "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/openmeter/productcatalog" + "github.com/openmeterio/openmeter/pkg/filter" "github.com/openmeterio/openmeter/pkg/framework/transaction" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" @@ -367,7 +368,7 @@ func (s *Service) ProvisionDefaultBillingProfile(ctx context.Context, namespace // Sandbox apps sandboxAppList, err := s.appService.ListApps(ctx, app.ListAppInput{ Namespace: namespace, - Type: lo.ToPtr(app.AppTypeSandbox), + Type: &filter.FilterString{Eq: lo.ToPtr(string(app.AppTypeSandbox))}, }) if err != nil { return fmt.Errorf("error fetching sandbox apps: %w", err)