aiwaf 0.1.6__py3-none-any.whl → 0.1.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiwaf might be problematic. Click here for more details.

aiwaf/middleware.py CHANGED
@@ -5,7 +5,7 @@ import re
5
5
  import os
6
6
  import numpy as np
7
7
  import joblib
8
-
8
+ from django.db.models import UUIDField
9
9
  from collections import defaultdict
10
10
  from django.utils.deprecation import MiddlewareMixin
11
11
  from django.http import JsonResponse
@@ -18,6 +18,14 @@ from django.urls import get_resolver
18
18
  from .blacklist_manager import BlacklistManager
19
19
  from .models import DynamicKeyword
20
20
 
21
+ def is_exempt_path(path):
22
+ path = path.lower()
23
+ exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
24
+ for exempt in exempt_paths:
25
+ if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
26
+ return True
27
+ return False
28
+
21
29
  MODEL_PATH = getattr(
22
30
  settings,
23
31
  "AIWAF_MODEL_PATH",
@@ -43,25 +51,32 @@ def get_ip(request):
43
51
  class IPAndKeywordBlockMiddleware:
44
52
  def __init__(self, get_response):
45
53
  self.get_response = get_response
46
- self.url_patterns = self._collect_view_paths()
54
+ self.safe_prefixes = self._collect_safe_prefixes()
47
55
 
48
- def _collect_view_paths(self):
56
+ def _collect_safe_prefixes(self):
49
57
  resolver = get_resolver()
50
- patterns = set()
58
+ prefixes = set()
51
59
 
52
60
  def extract(patterns_list, prefix=""):
53
61
  for p in patterns_list:
54
- if hasattr(p, "url_patterns"):
62
+ if hasattr(p, "url_patterns"): # include()
63
+ full_prefix = (prefix + str(p.pattern)).strip("^/").split("/")[0]
64
+ prefixes.add(full_prefix)
55
65
  extract(p.url_patterns, prefix + str(p.pattern))
56
66
  else:
57
67
  pat = (prefix + str(p.pattern)).strip("^$")
58
- patterns.add(pat)
68
+ path_parts = pat.strip("/").split("/")
69
+ if path_parts:
70
+ prefixes.add(path_parts[0])
59
71
  extract(resolver.url_patterns)
60
- return patterns
72
+ return prefixes
61
73
 
62
74
  def __call__(self, request):
75
+ raw_path = request.path.lower()
76
+ if is_exempt_path(raw_path):
77
+ return self.get_response(request)
63
78
  ip = get_ip(request)
64
- path = request.path.lower()
79
+ path = raw_path.lstrip("/")
65
80
  if BlacklistManager.is_blocked(ip):
66
81
  return JsonResponse({"error": "blocked"}, status=403)
67
82
  segments = [seg for seg in re.split(r"\W+", path) if len(seg) > 3]
@@ -74,8 +89,10 @@ class IPAndKeywordBlockMiddleware:
74
89
  .values_list("keyword", flat=True)[: getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10)]
75
90
  )
76
91
  all_kw = set(STATIC_KW) | set(dynamic_top)
77
- safe_kw = {kw for kw in all_kw if any(kw in pat for pat in self.url_patterns)}
78
- suspicious_kw = all_kw - safe_kw
92
+ suspicious_kw = {
93
+ kw for kw in all_kw
94
+ if not any(path.startswith(prefix) for prefix in self.safe_prefixes if prefix)
95
+ }
79
96
  for seg in segments:
80
97
  if seg in suspicious_kw:
81
98
  BlacklistManager.block(ip, f"Keyword block: {seg}")
@@ -93,6 +110,8 @@ class RateLimitMiddleware:
93
110
  self.logs = defaultdict(list)
94
111
 
95
112
  def __call__(self, request):
113
+ if is_exempt_path(request.path):
114
+ return self.get_response(request)
96
115
  ip = get_ip(request)
97
116
  now = time.time()
98
117
  recs = [t for t in self.logs[ip] if now - t < self.WINDOW]
@@ -113,6 +132,8 @@ class AIAnomalyMiddleware(MiddlewareMixin):
113
132
  TOP_N = getattr(settings, "AIWAF_DYNAMIC_TOP_N", 10)
114
133
 
115
134
  def process_request(self, request):
135
+ if is_exempt_path(request.path):
136
+ return None
116
137
  ip = get_ip(request)
117
138
  if BlacklistManager.is_blocked(ip):
118
139
  return JsonResponse({"error": "blocked"}, status=403)
@@ -120,12 +141,9 @@ class AIAnomalyMiddleware(MiddlewareMixin):
120
141
  now = time.time()
121
142
  key = f"aiwaf:{ip}"
122
143
  data = cache.get(key, [])
123
- # TODO: you may want to capture real status & response_time in process_response
124
144
  data.append((now, request.path, 0, 0.0))
125
145
  data = [d for d in data if now - d[0] < self.WINDOW]
126
146
  cache.set(key, data, timeout=self.WINDOW)
127
-
128
- # update dynamic‐keyword counts
129
147
  for seg in re.split(r"\W+", request.path.lower()):
130
148
  if len(seg) > 3:
131
149
  obj, _ = DynamicKeyword.objects.get_or_create(keyword=seg)
@@ -133,8 +151,6 @@ class AIAnomalyMiddleware(MiddlewareMixin):
133
151
 
134
152
  if len(data) < 5:
135
153
  return None
136
-
137
- # pull top‐N dynamic tokens
138
154
  top_dynamic = list(
139
155
  DynamicKeyword.objects
140
156
  .order_by("-count")
@@ -159,6 +175,8 @@ class AIAnomalyMiddleware(MiddlewareMixin):
159
175
 
160
176
  class HoneypotMiddleware(MiddlewareMixin):
161
177
  def process_view(self, request, view_func, view_args, view_kwargs):
178
+ if is_exempt_path(request.path):
179
+ return None
162
180
  trap = request.POST.get(getattr(settings, "AIWAF_HONEYPOT_FIELD", "hp_field"), "")
163
181
  if trap:
164
182
  ip = get_ip(request)
@@ -169,6 +187,8 @@ class HoneypotMiddleware(MiddlewareMixin):
169
187
 
170
188
  class UUIDTamperMiddleware(MiddlewareMixin):
171
189
  def process_view(self, request, view_func, view_args, view_kwargs):
190
+ if is_exempt_path(request.path):
191
+ return None
172
192
  uid = view_kwargs.get("uuid")
173
193
  if not uid:
174
194
  return None
@@ -177,8 +197,12 @@ class UUIDTamperMiddleware(MiddlewareMixin):
177
197
  app_label = view_func.__module__.split(".")[0]
178
198
  app_cfg = apps.get_app_config(app_label)
179
199
  for Model in app_cfg.get_models():
180
- if Model.objects.filter(pk=uid).exists():
181
- return None
200
+ if isinstance(Model._meta.pk, UUIDField):
201
+ try:
202
+ if Model.objects.filter(pk=uid).exists():
203
+ return None
204
+ except (ValueError, TypeError):
205
+ continue
182
206
 
183
207
  BlacklistManager.block(ip, "UUID tampering")
184
208
  return JsonResponse({"error": "blocked"}, status=403)
aiwaf/trainer.py CHANGED
@@ -12,6 +12,8 @@ import joblib
12
12
  from django.conf import settings
13
13
  from django.apps import apps
14
14
  from django.db.models import F
15
+ from django.urls import get_resolver
16
+
15
17
 
16
18
  LOG_PATH = settings.AIWAF_ACCESS_LOG
17
19
  MODEL_PATH = os.path.join(os.path.dirname(__file__), "resources", "model.pkl")
@@ -27,6 +29,45 @@ _LOG_RX = re.compile(
27
29
  BlacklistEntry = apps.get_model("aiwaf", "BlacklistEntry")
28
30
  DynamicKeyword = apps.get_model("aiwaf", "DynamicKeyword")
29
31
 
32
+ def is_exempt_path(path):
33
+ path = path.lower()
34
+ exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
35
+ for exempt in exempt_paths:
36
+ if path == exempt or path.startswith(exempt.rstrip("/") + "/"):
37
+ return True
38
+ return False
39
+
40
+ def path_exists_in_django(path):
41
+ from django.urls import get_resolver
42
+ from django.urls.resolvers import URLPattern, URLResolver
43
+
44
+ path = path.split("?")[0].lstrip("/")
45
+ try:
46
+ get_resolver().resolve(f"/{path}")
47
+ return True
48
+ except:
49
+ pass
50
+ parts = path.split("/")
51
+ root_resolver = get_resolver()
52
+ for pattern in root_resolver.url_patterns:
53
+ if isinstance(pattern, URLResolver):
54
+ prefix = pattern.pattern.describe().strip("^/")
55
+ if prefix and path.startswith(prefix):
56
+ return True
57
+ return False
58
+
59
+ def remove_exempt_keywords():
60
+ exempt_paths = getattr(settings, "AIWAF_EXEMPT_PATHS", [])
61
+ exempt_tokens = set()
62
+
63
+ for path in exempt_paths:
64
+ path = path.strip("/").lower()
65
+ segments = re.split(r"\W+", path)
66
+ exempt_tokens.update(seg for seg in segments if len(seg) > 3)
67
+
68
+ if exempt_tokens:
69
+ deleted_count, _ = DynamicKeyword.objects.filter(keyword__in=exempt_tokens).delete()
70
+ print(f"Removed {deleted_count} dynamic keywords that are now exempt: {list(exempt_tokens)}")
30
71
 
31
72
  def _read_all_logs():
32
73
  lines = []
@@ -62,10 +103,12 @@ def _parse(line):
62
103
  }
63
104
 
64
105
 
106
+
65
107
  def train():
108
+ remove_exempt_keywords()
66
109
  raw_lines = _read_all_logs()
67
110
  if not raw_lines:
68
- print(" No log lines found – check AIWAF_ACCESS_LOG setting.")
111
+ print("No log lines found – check AIWAF_ACCESS_LOG setting.")
69
112
  return
70
113
  parsed = []
71
114
  ip_404 = defaultdict(int)
@@ -89,6 +132,7 @@ def train():
89
132
  blocked_404.append(ip)
90
133
  if blocked_404:
91
134
  print(f"Blocked {len(blocked_404)} IPs for 404 flood: {blocked_404}")
135
+
92
136
  feature_dicts = []
93
137
  for r in parsed:
94
138
  ip = r["ip"]
@@ -97,7 +141,10 @@ def train():
97
141
  if (r["timestamp"] - t).total_seconds() <= 10
98
142
  )
99
143
  total404 = ip_404[ip]
100
- kw_hits = sum(k in r["path"].lower() for k in STATIC_KW)
144
+ is_known_path = path_exists_in_django(r["path"])
145
+ kw_hits = 0
146
+ if not is_known_path and not is_exempt_path(r["path"]):
147
+ kw_hits = sum(k in r["path"].lower() for k in STATIC_KW)
101
148
  status_idx = STATUS_IDX.index(r["status"]) if r["status"] in STATUS_IDX else -1
102
149
  feature_dicts.append({
103
150
  "ip": ip,
@@ -112,7 +159,6 @@ def train():
112
159
  if not feature_dicts:
113
160
  print("⚠️ Nothing to train on – no valid log entries.")
114
161
  return
115
-
116
162
  df = pd.DataFrame(feature_dicts)
117
163
  feature_cols = [c for c in df.columns if c != "ip"]
118
164
  X = df[feature_cols].astype(float).values
@@ -120,8 +166,8 @@ def train():
120
166
  model.fit(X)
121
167
  os.makedirs(os.path.dirname(MODEL_PATH), exist_ok=True)
122
168
  joblib.dump(model, MODEL_PATH)
123
- print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
124
- preds = model.predict(X) # -1 for outliers
169
+ print(f"Model trained on {len(X)} samples → {MODEL_PATH}")
170
+ preds = model.predict(X)
125
171
  anomalous_ips = set(df.loc[preds == -1, 'ip'])
126
172
  blocked_anom = []
127
173
  for ip in anomalous_ips:
@@ -132,20 +178,22 @@ def train():
132
178
  if created:
133
179
  blocked_anom.append(ip)
134
180
  if blocked_anom:
135
- print(f" Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
136
-
181
+ print(f"🚫 Blocked {len(blocked_anom)} anomalous IPs: {blocked_anom}")
137
182
  tokens = Counter()
138
183
  for r in parsed:
139
- if r["status"].startswith(("4", "5")):
184
+ if r["status"].startswith(("4", "5")) and not path_exists_in_django(r["path"]):
140
185
  for seg in re.split(r"\W+", r["path"].lower()):
141
186
  if len(seg) > 3 and seg not in STATIC_KW:
142
187
  tokens[seg] += 1
188
+
143
189
  top_tokens = tokens.most_common(10)
144
190
  for kw, cnt in top_tokens:
145
191
  obj, _ = DynamicKeyword.objects.get_or_create(keyword=kw)
146
192
  DynamicKeyword.objects.filter(pk=obj.pk).update(count=F("count") + cnt)
147
- print(f"DynamicKeyword DB updated with top tokens: {[kw for kw, _ in top_tokens]}")
193
+
194
+ print(f" DynamicKeyword DB updated with top tokens: {[kw for kw, _ in top_tokens]}")
195
+
148
196
 
149
197
 
150
198
  if __name__ == "__main__":
151
- train()
199
+ train()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.6
3
+ Version: 0.1.7.1
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -15,14 +15,14 @@ Dynamic: license-file
15
15
  Dynamic: requires-python
16
16
 
17
17
 
18
- # AI‑WAF
18
+ # AI‑WAF
19
19
 
20
20
  > A self‑learning, Django‑friendly Web Application Firewall
21
- > with rate‑limiting, anomaly detection, honeypots, UUID‑tamper protection, dynamic keyword extraction, file‑extension probing detection, and daily retraining.
21
+ > with rate‑limiting, anomaly detection, honeypots, UUID‑tamper protection, dynamic keyword extraction, file‑extension probing detection, exempt path awareness, and daily retraining.
22
22
 
23
23
  ---
24
24
 
25
- ## Package Structure
25
+ ## 📁 Package Structure
26
26
 
27
27
  ```
28
28
  aiwaf/
@@ -44,7 +44,7 @@ aiwaf/
44
44
 
45
45
  ---
46
46
 
47
- ## Features
47
+ ## 🚀 Features
48
48
 
49
49
  - **IP Blocklist**
50
50
  Instantly blocks suspicious IPs (supports CSV fallback or Django model).
@@ -53,7 +53,7 @@ aiwaf/
53
53
  Sliding‑window blocks flooders (> `AIWAF_RATE_MAX` per `AIWAF_RATE_WINDOW`), then blacklists them.
54
54
 
55
55
  - **AI Anomaly Detection**
56
- IsolationForest on features:
56
+ IsolationForest trained on:
57
57
  - Path length
58
58
  - Keyword hits (static + dynamic)
59
59
  - Response time
@@ -61,34 +61,28 @@ aiwaf/
61
61
  - Burst count
62
62
  - Total 404s
63
63
 
64
- - **Dynamic Keyword Extraction**
65
- Every retrain: top 10 most frequent “words” from 4xx/5xx paths are appended to your malicious keyword set.
64
+ - **Dynamic Keyword Extraction & Cleanup**
65
+ - Every retrain adds top 10 keyword segments from 4xx/5xx paths
66
+ - **If a path is added to `AIWAF_EXEMPT_PATHS`, its keywords are automatically removed from the database**
66
67
 
67
68
  - **File‑Extension Probing Detection**
68
- Tracks repeated 404s on common web‑extensions (e.g. `.php`, `.asp`) and auto‑blocks after a burst.
69
+ Tracks repeated 404s on common extensions (e.g. `.php`, `.asp`) and blocks IPs.
69
70
 
70
71
  - **Honeypot Field**
71
- Hidden form field (via template tag) that bots fill instant block.
72
+ Hidden field for bot detection IP blacklisted on fill.
72
73
 
73
74
  - **UUID Tampering Protection**
74
- Any `<uuid:…>` URL that doesn’t map to **any** model in its Django app gets blocked.
75
+ Blocks guessed or invalid UUIDs that don’t resolve to real models.
75
76
 
76
- - **Daily Retraining**
77
- Reads rotated/gzipped logs, auto‑blocks 404 floods (≥6), retrains the model, updates `model.pkl` + `dynamic_keywords.json`.
78
-
79
- ---
80
-
81
- ## Installation
82
-
83
- ```bash
84
- # From PyPI
85
- pip install aiwaf
77
+ - **Exempt Path Awareness**
78
+ Fully respects `AIWAF_EXEMPT_PATHS` across all modules exempt paths are:
79
+ - Skipped from keyword learning
80
+ - Immune to AI blocking
81
+ - Ignored in log training
82
+ - Cleaned from `DynamicKeyword` model automatically
86
83
 
87
- # Or for local development
88
- git clone https://github.com/aayushgauba/aiwaf.git
89
- cd aiwaf
90
- pip install -e .
91
- ```
84
+ - **Daily Retraining**
85
+ Reads rotated logs, auto‑blocks 404 floods, retrains the IsolationForest, updates `model.pkl`, and evolves the keyword DB.
92
86
 
93
87
  ---
94
88
 
@@ -96,33 +90,51 @@ pip install -e .
96
90
 
97
91
  ```python
98
92
  INSTALLED_APPS += ["aiwaf"]
93
+ ```
99
94
 
100
95
  ### Database Setup
101
96
 
102
- After adding `aiwaf` to your `INSTALLED_APPS`, create the necessary tables for the IP‐blacklist and dynamic‐keyword models:
97
+ After adding `aiwaf` to your `INSTALLED_APPS`, run the following to create the necessary tables:
103
98
 
104
99
  ```bash
105
100
  python manage.py makemigrations aiwaf
106
101
  python manage.py migrate
102
+ ```
107
103
 
108
- # Required
104
+ ---
105
+
106
+ ### Required
107
+
108
+ ```python
109
109
  AIWAF_ACCESS_LOG = "/var/log/nginx/access.log"
110
+ ```
111
+
112
+ ---
113
+
114
+ ### Optional (defaults shown)
110
115
 
111
- # Optional (defaults shown)
116
+ ```python
112
117
  AIWAF_MODEL_PATH = BASE_DIR / "aiwaf" / "resources" / "model.pkl"
113
118
  AIWAF_HONEYPOT_FIELD = "hp_field"
114
119
  AIWAF_RATE_WINDOW = 10 # seconds
115
- AIWAF_RATE_MAX = 20 # max reqs/window
120
+ AIWAF_RATE_MAX = 20 # max requests per window
116
121
  AIWAF_RATE_FLOOD = 10 # flood threshold
117
- AIWAF_WINDOW_SECONDS = 60 # anomaly window
118
- AIWAF_FILE_EXTENSIONS = [".php", ".asp", ".jsp"] # 404‑burst tracked extensions
122
+ AIWAF_WINDOW_SECONDS = 60 # anomaly detection window
123
+ AIWAF_FILE_EXTENSIONS = [".php", ".asp", ".jsp"]
124
+ AIWAF_EXEMPT_PATHS = [ # optional but highly recommended
125
+ "/favicon.ico",
126
+ "/robots.txt",
127
+ "/static/",
128
+ "/media/",
129
+ "/health/",
130
+ ]
119
131
  ```
120
132
 
121
- > **Note:** You no longer need to define `AIWAF_MALICIOUS_KEYWORDS` or `AIWAF_STATUS_CODES` in your settings — they’re built in and evolve dynamically.
133
+ > **Note:** You no longer need to define `AIWAF_MALICIOUS_KEYWORDS` or `AIWAF_STATUS_CODES` — they evolve dynamically.
122
134
 
123
135
  ---
124
136
 
125
- ## Middleware Setup
137
+ ## 🧱 Middleware Setup
126
138
 
127
139
  Add in **this** order to your `MIDDLEWARE` list:
128
140
 
@@ -139,7 +151,7 @@ MIDDLEWARE = [
139
151
 
140
152
  ---
141
153
 
142
- ## Honeypot Field (in your template)
154
+ ## 🕵️ Honeypot Field (in your template)
143
155
 
144
156
  ```django
145
157
  {% load aiwaf_tags %}
@@ -156,22 +168,23 @@ MIDDLEWARE = [
156
168
 
157
169
  ---
158
170
 
159
- ## Running Detection & Training
171
+ ## 🔁 Running Detection & Training
160
172
 
161
173
  ```bash
162
174
  python manage.py detect_and_train
163
175
  ```
164
176
 
165
- **What happens:**
166
- 1. Read access logs
177
+ ### What happens:
178
+ 1. Read access logs (incl. rotated or gzipped)
167
179
  2. Auto‑block IPs with ≥ 6 total 404s
168
180
  3. Extract features & train IsolationForest
169
181
  4. Save `model.pkl`
170
182
  5. Extract top 10 dynamic keywords from 4xx/5xx
183
+ 6. Remove any keywords associated with newly exempt paths
171
184
 
172
185
  ---
173
186
 
174
- ## How It Works
187
+ ## 🧠 How It Works
175
188
 
176
189
  | Middleware | Purpose |
177
190
  |------------------------------------|-----------------------------------------------------------------|
@@ -180,15 +193,16 @@ python manage.py detect_and_train
180
193
  | AIAnomalyMiddleware | ML‑driven behavior analysis + block on anomaly |
181
194
  | HoneypotMiddleware | Detects bots filling hidden inputs in forms |
182
195
  | UUIDTamperMiddleware | Blocks guessed/nonexistent UUIDs across all models in an app |
196
+
183
197
  ---
184
198
 
185
- ## License
199
+ ## 📄 License
186
200
 
187
201
  This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
188
202
 
189
203
  ---
190
204
 
191
- ## Credits
205
+ ## 👤 Credits
192
206
 
193
207
  **AI‑WAF** by [Aayush Gauba](https://github.com/aayushgauba)
194
208
  > “Let your firewall learn and evolve — keep your site a fortress.”
@@ -1,10 +1,10 @@
1
1
  aiwaf/__init__.py,sha256=nQFpJ1YpX48snzLjEQCf8zD2YNh8v0b_kPTrXx8uBYc,46
2
2
  aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
3
3
  aiwaf/blacklist_manager.py,sha256=sM6uTH7zD6MOPGb0kzqV2aFut2vxKgft_UVeRJr7klw,392
4
- aiwaf/middleware.py,sha256=04AbNgkwLMaYSiuEtw59A-O02tt4cqaKmP7XDNlkIG0,6359
4
+ aiwaf/middleware.py,sha256=LTLHmQYIQ36WwfR9FEPLrmTbYgqxIh4X5Aen4VJ-vN0,7350
5
5
  aiwaf/models.py,sha256=8au1umopgCo0lthztTTRrYRJQUM7uX8eAeXgs3z45K4,1282
6
6
  aiwaf/storage.py,sha256=bxCILzzvA1-q6nwclRE8WrfoRhe25H4VrsQDf0hl_lY,1903
7
- aiwaf/trainer.py,sha256=TKWJZzWTg892vdoSGWdCA0i-dKof2b29buWqJUrkr6k,4820
7
+ aiwaf/trainer.py,sha256=ir5kFTeLQuhMd2h094ct03Wr-rNZsX-mZHwjLx29F54,6422
8
8
  aiwaf/utils.py,sha256=RkEUWhhHy6tOk7V0UYv3cN4xhOR_7aBy9bjhwuV2cdA,1436
9
9
  aiwaf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -12,8 +12,8 @@ aiwaf/management/commands/detect_and_train.py,sha256=-o-LZ7QZ5GeJPCekryox1DGXKMm
12
12
  aiwaf/resources/model.pkl,sha256=rCCXH38SJrnaOba2WZrU1LQVzWT34x6bTVkq20XJU-Q,1091129
13
13
  aiwaf/template_tags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  aiwaf/template_tags/aiwaf_tags.py,sha256=1KGqeioYmgKACDUiPkykSqI7DLQ6-Ypy1k00weWj9iY,399
15
- aiwaf-0.1.6.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
16
- aiwaf-0.1.6.dist-info/METADATA,sha256=xwayhSMTf_thMyNrS2-E5Wa8S1vXpqiOuwRZiI8-6Pw,5414
17
- aiwaf-0.1.6.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
- aiwaf-0.1.6.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
19
- aiwaf-0.1.6.dist-info/RECORD,,
15
+ aiwaf-0.1.7.1.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
16
+ aiwaf-0.1.7.1.dist-info/METADATA,sha256=aO_1D_qSP_s4vKUj60a8VmsFcCLCyhBZii1tpbo3HqE,5790
17
+ aiwaf-0.1.7.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
18
+ aiwaf-0.1.7.1.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
19
+ aiwaf-0.1.7.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5