layercall 1.0.0__tar.gz

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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: layercall
3
+ Version: 1.0.0
4
+ Summary: Official LayerCall client — score an IP, email, phone, domain or a whole signup for fraud in one call. Zero dependencies.
5
+ Author: LayerCall
6
+ License: MIT
7
+ Project-URL: Homepage, https://www.layercall.com
8
+ Project-URL: Documentation, https://www.layercall.com/docs
9
+ Project-URL: Source, https://github.com/hirajha/layercall-sdk
10
+ Keywords: fraud,fraud-detection,vpn-detection,proxy-detection,tor,email-validation,disposable-email,phone-validation,ip-lookup,bot-detection,signup-fraud,risk-score
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # layercall
21
+
22
+ Official Python client for [LayerCall](https://www.layercall.com) — score an
23
+ IP, email, phone number, domain, or a whole signup for fraud in a single call.
24
+
25
+ **Zero dependencies.** Standard library only. A trust check sits on your signup
26
+ path, which is the worst place to add a dependency tree that has to resolve
27
+ against whatever your app already pins.
28
+
29
+ ```bash
30
+ pip install layercall
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ import os
37
+ from layercall import LayerCall
38
+
39
+ lc = LayerCall(os.environ["LAYERCALL_API_KEY"])
40
+
41
+ result = lc.score_user(ip=request.remote_addr, email=form["email"])
42
+
43
+ if result["verdict"] == "review":
44
+ send_otp(form["email"]) # a real user clears it themselves
45
+ elif result["verdict"] == "block":
46
+ queue_for_review(result)
47
+ ```
48
+
49
+ [Get a free API key](https://www.layercall.com/get-key) — 1,000 lookups a
50
+ month, no card.
51
+
52
+ ## Acting on a verdict
53
+
54
+ | Verdict | Do this | Not this |
55
+ | --- | --- | --- |
56
+ | `allow` | Let them through | — |
57
+ | `review` | **Step up** — OTP, SMS, 3-D Secure | Don't reject. This band includes ordinary VPN users. |
58
+ | `block` | Reject, or send to a human queue | Don't reject silently |
59
+
60
+ Prefer a challenge over a rejection. A real user clears it in seconds; an
61
+ attacker cannot. A false positive then costs friction instead of a customer.
62
+
63
+ **Already send an OTP to everyone?** Passwordless products can't use an email
64
+ code as a step-up — it's already mandatory. Score after sign-in and use a
65
+ different lever: a limited account state, a delayed payout, or a review queue.
66
+
67
+ ## Methods
68
+
69
+ ```python
70
+ lc.score_ip("185.220.101.1")
71
+ lc.verify_email("someone@mailinator.com")
72
+ lc.lookup_phone("+14155552671")
73
+ lc.lookup_phone("4155552671", country="US")
74
+ lc.score_domain("example.com")
75
+ lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
76
+ lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
77
+ ```
78
+
79
+ Every method takes `strictness` (`0` lenient → `3` paranoid). It moves the
80
+ verdict thresholds only; the risk score never changes, so you can re-tune
81
+ without re-scoring anything.
82
+
83
+ ## None means unknown, never "no"
84
+
85
+ ```python
86
+ signals = lc.score_domain("example.de")["signals"]
87
+ signals["newly_registered"] # None — .de publishes no RDAP, so age is unknown
88
+ ```
89
+
90
+ `None` means we could not determine it. It is **not** a negative finding.
91
+ Treating it as "established" is exactly the mistake the field exists to
92
+ prevent.
93
+
94
+ Same for `mailbox_exists`: Gmail and Yahoo accept mail for addresses that do
95
+ not exist, so we return `None` rather than a guess.
96
+
97
+ ## Errors
98
+
99
+ ```python
100
+ from layercall import LayerCallError
101
+
102
+ try:
103
+ lc.score_ip(ip)
104
+ except LayerCallError as err:
105
+ if err.is_quota: # 402 — out of quota or spend cap reached
106
+ ...
107
+ if err.is_auth: # 401/403 — bad or revoked key
108
+ ...
109
+ print(err.request_id) # quote this in a support ticket
110
+ except Exception:
111
+ pass # fail open: let the signup through
112
+ ```
113
+
114
+ 429s and 5xx retry automatically (2 attempts, short backoff). Other 4xx fail
115
+ fast — they will not succeed on a retry.
116
+
117
+ ## Server-side only
118
+
119
+ An API key in anything a user can read is public and bills to your account.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,104 @@
1
+ # layercall
2
+
3
+ Official Python client for [LayerCall](https://www.layercall.com) — score an
4
+ IP, email, phone number, domain, or a whole signup for fraud in a single call.
5
+
6
+ **Zero dependencies.** Standard library only. A trust check sits on your signup
7
+ path, which is the worst place to add a dependency tree that has to resolve
8
+ against whatever your app already pins.
9
+
10
+ ```bash
11
+ pip install layercall
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```python
17
+ import os
18
+ from layercall import LayerCall
19
+
20
+ lc = LayerCall(os.environ["LAYERCALL_API_KEY"])
21
+
22
+ result = lc.score_user(ip=request.remote_addr, email=form["email"])
23
+
24
+ if result["verdict"] == "review":
25
+ send_otp(form["email"]) # a real user clears it themselves
26
+ elif result["verdict"] == "block":
27
+ queue_for_review(result)
28
+ ```
29
+
30
+ [Get a free API key](https://www.layercall.com/get-key) — 1,000 lookups a
31
+ month, no card.
32
+
33
+ ## Acting on a verdict
34
+
35
+ | Verdict | Do this | Not this |
36
+ | --- | --- | --- |
37
+ | `allow` | Let them through | — |
38
+ | `review` | **Step up** — OTP, SMS, 3-D Secure | Don't reject. This band includes ordinary VPN users. |
39
+ | `block` | Reject, or send to a human queue | Don't reject silently |
40
+
41
+ Prefer a challenge over a rejection. A real user clears it in seconds; an
42
+ attacker cannot. A false positive then costs friction instead of a customer.
43
+
44
+ **Already send an OTP to everyone?** Passwordless products can't use an email
45
+ code as a step-up — it's already mandatory. Score after sign-in and use a
46
+ different lever: a limited account state, a delayed payout, or a review queue.
47
+
48
+ ## Methods
49
+
50
+ ```python
51
+ lc.score_ip("185.220.101.1")
52
+ lc.verify_email("someone@mailinator.com")
53
+ lc.lookup_phone("+14155552671")
54
+ lc.lookup_phone("4155552671", country="US")
55
+ lc.score_domain("example.com")
56
+ lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
57
+ lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
58
+ ```
59
+
60
+ Every method takes `strictness` (`0` lenient → `3` paranoid). It moves the
61
+ verdict thresholds only; the risk score never changes, so you can re-tune
62
+ without re-scoring anything.
63
+
64
+ ## None means unknown, never "no"
65
+
66
+ ```python
67
+ signals = lc.score_domain("example.de")["signals"]
68
+ signals["newly_registered"] # None — .de publishes no RDAP, so age is unknown
69
+ ```
70
+
71
+ `None` means we could not determine it. It is **not** a negative finding.
72
+ Treating it as "established" is exactly the mistake the field exists to
73
+ prevent.
74
+
75
+ Same for `mailbox_exists`: Gmail and Yahoo accept mail for addresses that do
76
+ not exist, so we return `None` rather than a guess.
77
+
78
+ ## Errors
79
+
80
+ ```python
81
+ from layercall import LayerCallError
82
+
83
+ try:
84
+ lc.score_ip(ip)
85
+ except LayerCallError as err:
86
+ if err.is_quota: # 402 — out of quota or spend cap reached
87
+ ...
88
+ if err.is_auth: # 401/403 — bad or revoked key
89
+ ...
90
+ print(err.request_id) # quote this in a support ticket
91
+ except Exception:
92
+ pass # fail open: let the signup through
93
+ ```
94
+
95
+ 429s and 5xx retry automatically (2 attempts, short backoff). Other 4xx fail
96
+ fast — they will not succeed on a retry.
97
+
98
+ ## Server-side only
99
+
100
+ An API key in anything a user can read is public and bills to your account.
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,231 @@
1
+ """Official LayerCall client.
2
+
3
+ Score an IP, email, phone number, domain, or a whole signup for fraud in a
4
+ single call.
5
+
6
+ Zero dependencies, standard library only. A trust check sits on the signup
7
+ path, which is the worst place in an application to introduce a dependency
8
+ tree that has to be resolved against whatever the host app already pins.
9
+
10
+ Test keys return synthetic data
11
+ -------------------------------
12
+ A key beginning ``tl_test_`` returns fabricated scores and signals, derived
13
+ from the value you sent rather than from anything known about it. Response
14
+ shapes, error codes and your own allow/block rules behave exactly as in
15
+ production, so integration tests against a test key are valid — the numbers
16
+ are not. Those responses carry ``test_mode: True`` and a ``test_mode_note``.
17
+
18
+ result = client.score_ip("8.8.8.8")
19
+ if result.get("test_mode"):
20
+ ... # pointed at a test key, do not assert on risk_score
21
+
22
+ Use a ``tl_live_`` key to score real values.
23
+ See https://www.layercall.com/docs/test-mode
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import time
30
+ import urllib.error
31
+ import urllib.parse
32
+ import urllib.request
33
+ from typing import Any, Literal, Mapping, Sequence
34
+
35
+ __version__ = "1.0.0"
36
+ __all__ = ["LayerCall", "LayerCallError"]
37
+
38
+ Verdict = Literal["allow", "review", "block"]
39
+ Strictness = Literal[0, 1, 2, 3]
40
+
41
+ _DEFAULT_BASE = "https://www.layercall.com"
42
+
43
+
44
+ class LayerCallError(Exception):
45
+ """A non-2xx response. Branch on ``status`` or ``code``."""
46
+
47
+ def __init__(
48
+ self,
49
+ message: str,
50
+ status: int,
51
+ code: str | None = None,
52
+ request_id: str | None = None,
53
+ ) -> None:
54
+ super().__init__(message)
55
+ self.status = status
56
+ self.code = code
57
+ self.request_id = request_id
58
+
59
+ @property
60
+ def is_quota(self) -> bool:
61
+ """Quota exhausted or spend cap reached — upgrade or raise the cap."""
62
+ return self.status == 402
63
+
64
+ @property
65
+ def is_auth(self) -> bool:
66
+ """Bad or revoked key."""
67
+ return self.status in (401, 403)
68
+
69
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
70
+ return f"LayerCallError(status={self.status}, code={self.code!r}, message={self!s})"
71
+
72
+
73
+ class LayerCall:
74
+ """Client for the LayerCall API.
75
+
76
+ Server-side only. An API key in anything a user can read is public, and it
77
+ bills to your account.
78
+
79
+ lc = LayerCall(os.environ["LAYERCALL_API_KEY"])
80
+ result = lc.score_user(ip=request.remote_addr, email=form["email"])
81
+ if result["verdict"] == "review":
82
+ send_otp(form["email"]) # a real user clears it themselves
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ api_key: str,
88
+ *,
89
+ base_url: str = _DEFAULT_BASE,
90
+ timeout: float = 5.0,
91
+ retries: int = 2,
92
+ ) -> None:
93
+ if not api_key:
94
+ raise ValueError("LayerCall: api_key is required.")
95
+ self._key = api_key
96
+ self._base = base_url.rstrip("/")
97
+ self._timeout = timeout
98
+ self._retries = retries
99
+
100
+ # -- transport --------------------------------------------------------
101
+
102
+ def _request(
103
+ self,
104
+ path: str,
105
+ *,
106
+ query: Mapping[str, Any] | None = None,
107
+ body: Mapping[str, Any] | None = None,
108
+ ) -> dict[str, Any]:
109
+ url = self._base + path
110
+ if query:
111
+ clean = {k: v for k, v in query.items() if v is not None}
112
+ if clean:
113
+ url += "?" + urllib.parse.urlencode(clean)
114
+
115
+ data = json.dumps(body).encode() if body is not None else None
116
+ last_exc: Exception | None = None
117
+
118
+ for attempt in range(self._retries + 1):
119
+ req = urllib.request.Request(
120
+ url,
121
+ data=data,
122
+ method="POST" if data is not None else "GET",
123
+ headers={
124
+ "X-Api-Key": self._key,
125
+ "Content-Type": "application/json",
126
+ "User-Agent": f"layercall-python/{__version__}",
127
+ },
128
+ )
129
+ try:
130
+ with urllib.request.urlopen(req, timeout=self._timeout) as resp:
131
+ return json.loads(resp.read().decode() or "{}")
132
+ except urllib.error.HTTPError as exc:
133
+ try:
134
+ payload = json.loads(exc.read().decode() or "{}")
135
+ except Exception:
136
+ payload = {}
137
+ err = LayerCallError(
138
+ payload.get("error") or f"HTTP {exc.code}",
139
+ exc.code,
140
+ payload.get("code"),
141
+ payload.get("request_id"),
142
+ )
143
+ # A 4xx other than 429 fails identically on retry. Surfacing it
144
+ # now beats spending a signup path's latency budget proving it.
145
+ if not (exc.code == 429 or exc.code >= 500) or attempt == self._retries:
146
+ raise err from None
147
+ last_exc = err
148
+ except Exception as exc: # timeouts, DNS, connection resets
149
+ last_exc = exc
150
+ if attempt == self._retries:
151
+ break
152
+ # Short exponential backoff — this sits in front of a user.
153
+ time.sleep(0.15 * (2**attempt))
154
+
155
+ raise last_exc if last_exc else RuntimeError("LayerCall: request failed")
156
+
157
+ # -- endpoints --------------------------------------------------------
158
+
159
+ def score_ip(self, ip: str, *, strictness: Strictness | None = None) -> dict[str, Any]:
160
+ """VPN, proxy, Tor, datacenter, geolocation and ASN for an IP."""
161
+ return self._request("/v1/score/ip", query={"ip": ip, "strictness": strictness})
162
+
163
+ def verify_email(self, email: str, *, strictness: Strictness | None = None) -> dict[str, Any]:
164
+ """Syntax, MX, disposable, role account, homograph and domain age."""
165
+ return self._request("/v1/verify/email", query={"email": email, "strictness": strictness})
166
+
167
+ def lookup_phone(
168
+ self,
169
+ phone: str,
170
+ *,
171
+ country: str | None = None,
172
+ strictness: Strictness | None = None,
173
+ ) -> dict[str, Any]:
174
+ """Numbering-plan validation worldwide. ``country`` only for national format."""
175
+ return self._request(
176
+ "/v1/lookup/phone",
177
+ query={"phone": phone, "country": country, "strictness": strictness},
178
+ )
179
+
180
+ def score_domain(self, domain: str) -> dict[str, Any]:
181
+ """RDAP registration date, registrar, MX/SPF/DMARC, risky TLD."""
182
+ return self._request("/v1/score/domain", query={"domain": domain})
183
+
184
+ def score_user(
185
+ self,
186
+ *,
187
+ ip: str | None = None,
188
+ email: str | None = None,
189
+ phone: str | None = None,
190
+ phone_country: str | None = None,
191
+ domain: str | None = None,
192
+ device_id: str | None = None,
193
+ strictness: Strictness | None = None,
194
+ ) -> dict[str, Any]:
195
+ """Score a whole signup. A hard block on one component is never averaged away."""
196
+ payload = {
197
+ k: v
198
+ for k, v in {
199
+ "ip": ip,
200
+ "email": email,
201
+ "phone": phone,
202
+ "phone_country": phone_country,
203
+ "domain": domain,
204
+ "device_id": device_id,
205
+ "strictness": strictness,
206
+ }.items()
207
+ if v is not None
208
+ }
209
+ if not payload:
210
+ raise ValueError("score_user: provide at least one of ip, email or phone.")
211
+ return self._request("/v1/score/user", body=payload)
212
+
213
+ def batch(
214
+ self,
215
+ type: Literal["ip", "email", "phone", "domain"],
216
+ items: Sequence[str],
217
+ *,
218
+ strictness: Strictness | None = None,
219
+ phone_country: str | None = None,
220
+ ) -> dict[str, Any]:
221
+ """Up to 500 values of one type. Each item carries its own error."""
222
+ if len(items) > 500:
223
+ raise ValueError(
224
+ f"batch: {len(items)} items exceeds the limit of 500 — send several batches."
225
+ )
226
+ payload: dict[str, Any] = {"type": type, "items": list(items)}
227
+ if strictness is not None:
228
+ payload["strictness"] = strictness
229
+ if phone_country is not None:
230
+ payload["phone_country"] = phone_country
231
+ return self._request("/v1/batch", body=payload)
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: layercall
3
+ Version: 1.0.0
4
+ Summary: Official LayerCall client — score an IP, email, phone, domain or a whole signup for fraud in one call. Zero dependencies.
5
+ Author: LayerCall
6
+ License: MIT
7
+ Project-URL: Homepage, https://www.layercall.com
8
+ Project-URL: Documentation, https://www.layercall.com/docs
9
+ Project-URL: Source, https://github.com/hirajha/layercall-sdk
10
+ Keywords: fraud,fraud-detection,vpn-detection,proxy-detection,tor,email-validation,disposable-email,phone-validation,ip-lookup,bot-detection,signup-fraud,risk-score
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # layercall
21
+
22
+ Official Python client for [LayerCall](https://www.layercall.com) — score an
23
+ IP, email, phone number, domain, or a whole signup for fraud in a single call.
24
+
25
+ **Zero dependencies.** Standard library only. A trust check sits on your signup
26
+ path, which is the worst place to add a dependency tree that has to resolve
27
+ against whatever your app already pins.
28
+
29
+ ```bash
30
+ pip install layercall
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ import os
37
+ from layercall import LayerCall
38
+
39
+ lc = LayerCall(os.environ["LAYERCALL_API_KEY"])
40
+
41
+ result = lc.score_user(ip=request.remote_addr, email=form["email"])
42
+
43
+ if result["verdict"] == "review":
44
+ send_otp(form["email"]) # a real user clears it themselves
45
+ elif result["verdict"] == "block":
46
+ queue_for_review(result)
47
+ ```
48
+
49
+ [Get a free API key](https://www.layercall.com/get-key) — 1,000 lookups a
50
+ month, no card.
51
+
52
+ ## Acting on a verdict
53
+
54
+ | Verdict | Do this | Not this |
55
+ | --- | --- | --- |
56
+ | `allow` | Let them through | — |
57
+ | `review` | **Step up** — OTP, SMS, 3-D Secure | Don't reject. This band includes ordinary VPN users. |
58
+ | `block` | Reject, or send to a human queue | Don't reject silently |
59
+
60
+ Prefer a challenge over a rejection. A real user clears it in seconds; an
61
+ attacker cannot. A false positive then costs friction instead of a customer.
62
+
63
+ **Already send an OTP to everyone?** Passwordless products can't use an email
64
+ code as a step-up — it's already mandatory. Score after sign-in and use a
65
+ different lever: a limited account state, a delayed payout, or a review queue.
66
+
67
+ ## Methods
68
+
69
+ ```python
70
+ lc.score_ip("185.220.101.1")
71
+ lc.verify_email("someone@mailinator.com")
72
+ lc.lookup_phone("+14155552671")
73
+ lc.lookup_phone("4155552671", country="US")
74
+ lc.score_domain("example.com")
75
+ lc.score_user(ip=ip, email=email, phone=phone, device_id=device_id)
76
+ lc.batch("email", ["a@x.com", "b@y.com"]) # up to 500
77
+ ```
78
+
79
+ Every method takes `strictness` (`0` lenient → `3` paranoid). It moves the
80
+ verdict thresholds only; the risk score never changes, so you can re-tune
81
+ without re-scoring anything.
82
+
83
+ ## None means unknown, never "no"
84
+
85
+ ```python
86
+ signals = lc.score_domain("example.de")["signals"]
87
+ signals["newly_registered"] # None — .de publishes no RDAP, so age is unknown
88
+ ```
89
+
90
+ `None` means we could not determine it. It is **not** a negative finding.
91
+ Treating it as "established" is exactly the mistake the field exists to
92
+ prevent.
93
+
94
+ Same for `mailbox_exists`: Gmail and Yahoo accept mail for addresses that do
95
+ not exist, so we return `None` rather than a guess.
96
+
97
+ ## Errors
98
+
99
+ ```python
100
+ from layercall import LayerCallError
101
+
102
+ try:
103
+ lc.score_ip(ip)
104
+ except LayerCallError as err:
105
+ if err.is_quota: # 402 — out of quota or spend cap reached
106
+ ...
107
+ if err.is_auth: # 401/403 — bad or revoked key
108
+ ...
109
+ print(err.request_id) # quote this in a support ticket
110
+ except Exception:
111
+ pass # fail open: let the signup through
112
+ ```
113
+
114
+ 429s and 5xx retry automatically (2 attempts, short backoff). Other 4xx fail
115
+ fast — they will not succeed on a retry.
116
+
117
+ ## Server-side only
118
+
119
+ An API key in anything a user can read is public and bills to your account.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,7 @@
1
+ README.md
2
+ pyproject.toml
3
+ layercall/__init__.py
4
+ layercall.egg-info/PKG-INFO
5
+ layercall.egg-info/SOURCES.txt
6
+ layercall.egg-info/dependency_links.txt
7
+ layercall.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ layercall
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "layercall"
7
+ version = "1.0.0"
8
+ description = "Official LayerCall client — score an IP, email, phone, domain or a whole signup for fraud in one call. Zero dependencies."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "LayerCall" }]
13
+ keywords = ["fraud", "fraud-detection", "vpn-detection", "proxy-detection", "tor", "email-validation", "disposable-email", "phone-validation", "ip-lookup", "bot-detection", "signup-fraud", "risk-score"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Security",
20
+ "Topic :: Internet :: WWW/HTTP",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://www.layercall.com"
26
+ Documentation = "https://www.layercall.com/docs"
27
+ Source = "https://github.com/hirajha/layercall-sdk"
28
+
29
+ [tool.setuptools.packages.find]
30
+ include = ["layercall*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+