cas-system-client 2.0.0__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.
cas_client/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """CAS SSO Client for Python"""
2
+ from .client import CasClient
3
+
4
+ __version__ = '2.0.0'
5
+ __all__ = ['CasClient']
cas_client/client.py ADDED
@@ -0,0 +1,264 @@
1
+ """
2
+ CAS SSO Client for Python
3
+ Provides SSO authentication, token validation, HMAC signing, and role management.
4
+ """
5
+
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ import time
10
+ import logging
11
+ from typing import Optional, Dict, List, Any
12
+ from urllib.parse import urlencode
13
+
14
+ import requests
15
+
16
+ logger = logging.getLogger('cas_client')
17
+
18
+
19
+ class CasClient:
20
+ """
21
+ CAS SSO Client for Python applications.
22
+
23
+ Args:
24
+ server_url: CAS server URL (e.g., https://your-cas-server.com). Used as
25
+ the internal/back-channel base for server-to-server calls
26
+ (token validation, logout, token generation).
27
+ client_id: Registered client ID
28
+ client_secret: Registered client secret
29
+ callback_url: OAuth callback URL
30
+ public_url: Public, browser-facing CAS base URL used ONLY when building
31
+ the browser login redirect (``get_login_url``). In split-horizon
32
+ deployments the browser must reach CAS at a public host while the
33
+ server reaches it at a different internal host. Optional; when empty
34
+ the login URL falls back to ``server_url`` (single-url dev).
35
+ signature_secret: HMAC signature secret (optional)
36
+ enable_signature_validation: Enable HMAC request signing (default: False)
37
+ timeout: Request timeout in seconds (default: 30)
38
+ verify_ssl: Verify SSL certificates (default: True)
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ server_url: str,
44
+ client_id: str,
45
+ client_secret: str,
46
+ callback_url: str = '',
47
+ public_url: str = '',
48
+ signature_secret: str = '',
49
+ enable_signature_validation: bool = False,
50
+ timeout: int = 30,
51
+ verify_ssl: bool = True,
52
+ ):
53
+ if not server_url:
54
+ raise ValueError('server_url is required')
55
+ if not client_id:
56
+ raise ValueError('client_id is required')
57
+ if not client_secret:
58
+ raise ValueError('client_secret is required')
59
+
60
+ self.config = {
61
+ 'server_url': server_url.rstrip('/'),
62
+ # Public/browser-facing base for the login redirect only. Falls back
63
+ # to server_url when not provided so single-url setups are unchanged.
64
+ 'public_url': (public_url or '').rstrip('/'),
65
+ 'client_id': client_id,
66
+ 'client_secret': client_secret,
67
+ 'callback_url': callback_url,
68
+ 'signature_secret': signature_secret or 'default-signature-secret',
69
+ 'enable_signature_validation': enable_signature_validation,
70
+ 'timeout': timeout,
71
+ 'verify_ssl': verify_ssl,
72
+ }
73
+
74
+ self._session = requests.Session()
75
+ self._session.headers.update({'Content-Type': 'application/json'})
76
+ self._session.verify = verify_ssl
77
+ self._session.timeout = timeout
78
+
79
+ # In-memory token cache
80
+ self._cache: Dict[str, Dict[str, Any]] = {}
81
+
82
+ def get_login_url(self, return_url: Optional[str] = None) -> str:
83
+ """Generate CAS SSO login URL.
84
+
85
+ The CAS server expects only ``client_id`` on the login redirect; it
86
+ looks up the client's registered callback URL server-side and appends
87
+ the token to it (``{callback_url}?token={JWT}``).
88
+
89
+ This is the URL the user's BROWSER is redirected to, so it is built from
90
+ ``public_url`` when configured (the public, browser-facing CAS host),
91
+ falling back to ``server_url`` otherwise. Server-to-server calls still
92
+ use ``server_url`` (the internal back-channel host).
93
+
94
+ Args:
95
+ return_url: Unused by the CAS server (the registered callback URL
96
+ is always used). Accepted for backwards compatibility.
97
+
98
+ Returns:
99
+ Full CAS login URL string
100
+ """
101
+ params = {
102
+ 'client_id': self.config['client_id'],
103
+ }
104
+ base = self.config['public_url'] or self.config['server_url']
105
+ return f"{base}/sso/login?{urlencode(params)}"
106
+
107
+ def generate_sso_token(self, username: str) -> Optional[Dict[str, Any]]:
108
+ """Generate SSO token for a user via client credentials.
109
+
110
+ Args:
111
+ username: Username to generate token for
112
+
113
+ Returns:
114
+ Token data dict with 'token' and 'redirect_url', or None
115
+ """
116
+ try:
117
+ timestamp = int(time.time())
118
+ request_data = {
119
+ 'client_id': self.config['client_id'],
120
+ 'client_secret': self.config['client_secret'],
121
+ 'username': username,
122
+ }
123
+
124
+ headers = {
125
+ 'X-Client-ID': self.config['client_id'],
126
+ 'X-Timestamp': str(timestamp),
127
+ }
128
+
129
+ if self.config['enable_signature_validation']:
130
+ headers['X-Signature'] = self._generate_signature(
131
+ 'POST', '/api/sso/token', request_data, timestamp
132
+ )
133
+
134
+ response = self._session.post(
135
+ f"{self.config['server_url']}/api/sso/token",
136
+ json=request_data,
137
+ headers=headers,
138
+ timeout=self.config['timeout'],
139
+ )
140
+
141
+ if response.status_code == 200:
142
+ data = response.json()
143
+ if 'token' in data:
144
+ return data
145
+
146
+ return None
147
+ except Exception as e:
148
+ logger.error(f'CAS SSO token generation failed: {e}')
149
+ return None
150
+
151
+ def validate_token(self, token: str) -> Optional[Dict[str, Any]]:
152
+ """Validate SSO token with CAS server.
153
+
154
+ Args:
155
+ token: JWT token to validate
156
+
157
+ Returns:
158
+ User data dict or None
159
+ """
160
+ try:
161
+ timestamp = int(time.time())
162
+ request_data = {
163
+ 'token': token,
164
+ 'client_id': self.config['client_id'],
165
+ 'client_secret': self.config['client_secret'],
166
+ }
167
+
168
+ headers = {
169
+ 'X-Client-ID': self.config['client_id'],
170
+ 'X-Timestamp': str(timestamp),
171
+ }
172
+
173
+ if self.config['enable_signature_validation']:
174
+ headers['X-Signature'] = self._generate_signature(
175
+ 'POST', '/api/sso/validate', request_data, timestamp
176
+ )
177
+
178
+ response = self._session.post(
179
+ f"{self.config['server_url']}/api/sso/validate",
180
+ json=request_data,
181
+ headers=headers,
182
+ timeout=self.config['timeout'],
183
+ )
184
+
185
+ if response.status_code == 200:
186
+ data = response.json()
187
+ # CAS server signals success with {"valid": true, "user": {...},
188
+ # "expires_at": "<datetime>"}. Honor the explicit "valid" flag.
189
+ if data.get('valid') and 'user' in data:
190
+ cache_key = hashlib.md5(token.encode()).hexdigest()
191
+ self._cache[cache_key] = {
192
+ 'data': data['user'],
193
+ 'expires': time.time() + 3600,
194
+ }
195
+ return data['user']
196
+
197
+ return None
198
+ except Exception as e:
199
+ logger.error(f'CAS token validation failed: {e}')
200
+ return None
201
+
202
+ def get_user_from_token(self, token: str) -> Optional[Dict[str, Any]]:
203
+ """Get cached user data from token.
204
+
205
+ Args:
206
+ token: JWT token
207
+
208
+ Returns:
209
+ Cached user data or None
210
+ """
211
+ cache_key = hashlib.md5(token.encode()).hexdigest()
212
+ cached = self._cache.get(cache_key)
213
+ if cached and cached['expires'] > time.time():
214
+ return cached['data']
215
+ self._cache.pop(cache_key, None)
216
+ return None
217
+
218
+ def logout(self, token: Optional[str] = None) -> bool:
219
+ """Logout from CAS server.
220
+
221
+ Args:
222
+ token: JWT token to invalidate cache for
223
+
224
+ Returns:
225
+ True if successful
226
+ """
227
+ try:
228
+ if token:
229
+ cache_key = hashlib.md5(token.encode()).hexdigest()
230
+ self._cache.pop(cache_key, None)
231
+
232
+ response = self._session.post(
233
+ f"{self.config['server_url']}/api/logout",
234
+ timeout=self.config['timeout'],
235
+ )
236
+ return response.status_code == 200
237
+ except Exception as e:
238
+ logger.error(f'CAS logout failed: {e}')
239
+ return False
240
+
241
+ @staticmethod
242
+ def user_has_role(user: Dict[str, Any], role: str) -> bool:
243
+ """Check if user has a specific role."""
244
+ return role in (user.get('roles') or [])
245
+
246
+ @staticmethod
247
+ def user_has_any_role(user: Dict[str, Any], roles: List[str]) -> bool:
248
+ """Check if user has any of the specified roles."""
249
+ user_roles = set(user.get('roles') or [])
250
+ return bool(user_roles & set(roles))
251
+
252
+ @staticmethod
253
+ def user_has_all_roles(user: Dict[str, Any], roles: List[str]) -> bool:
254
+ """Check if user has all specified roles."""
255
+ user_roles = set(user.get('roles') or [])
256
+ return set(roles).issubset(user_roles)
257
+
258
+ def _generate_signature(self, method: str, uri: str, data: dict, timestamp: int) -> str:
259
+ """Generate HMAC SHA-256 signature for request."""
260
+ body = json.dumps(data)
261
+ payload = '|'.join([method, uri, body, str(timestamp), self.config['client_id']])
262
+ secret = self.config['signature_secret']
263
+ signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
264
+ return f'sha256={signature}'
@@ -0,0 +1,172 @@
1
+ """
2
+ Django and Flask middleware for CAS SSO authentication.
3
+ """
4
+
5
+ import logging
6
+ from typing import Optional, List
7
+
8
+ logger = logging.getLogger('cas_client')
9
+
10
+
11
+ # =============================================================================
12
+ # Django Middleware
13
+ # =============================================================================
14
+
15
+ class DjangoCasMiddleware:
16
+ """
17
+ Django middleware for CAS authentication.
18
+
19
+ Add to MIDDLEWARE in settings.py:
20
+ 'cas_client.middleware.DjangoCasMiddleware',
21
+
22
+ Configure in settings.py:
23
+ CAS_CLIENT = CasClient(...)
24
+ CAS_PROTECTED_PATHS = ['/dashboard', '/admin']
25
+ CAS_LOGIN_URL = '/auth/login'
26
+ """
27
+
28
+ def __init__(self, get_response):
29
+ self.get_response = get_response
30
+
31
+ def __call__(self, request):
32
+ from django.conf import settings
33
+ from django.shortcuts import redirect
34
+ from django.http import JsonResponse
35
+
36
+ protected_paths = getattr(settings, 'CAS_PROTECTED_PATHS', [])
37
+ login_url = getattr(settings, 'CAS_LOGIN_URL', '/auth/login')
38
+ cas_client = getattr(settings, 'CAS_CLIENT', None)
39
+
40
+ is_protected = any(request.path.startswith(p) for p in protected_paths)
41
+
42
+ if is_protected:
43
+ cas_user = request.session.get('cas_user')
44
+
45
+ # Check Bearer token if no session user
46
+ if not cas_user and cas_client:
47
+ auth_header = request.META.get('HTTP_AUTHORIZATION', '')
48
+ if auth_header.startswith('Bearer '):
49
+ token = auth_header[7:]
50
+ cas_user = cas_client.get_user_from_token(token)
51
+ if not cas_user:
52
+ cas_user = cas_client.validate_token(token)
53
+ if cas_user:
54
+ request.session['cas_user'] = cas_user
55
+
56
+ if cas_user:
57
+ request.cas_user = cas_user
58
+ else:
59
+ if request.content_type == 'application/json':
60
+ return JsonResponse({'error': 'Authentication required'}, status=401)
61
+ return redirect(f'{login_url}?return_url={request.path}')
62
+
63
+ response = self.get_response(request)
64
+ return response
65
+
66
+
67
+ def django_role_required(*roles: str):
68
+ """
69
+ Django decorator to require specific roles.
70
+
71
+ Usage:
72
+ @django_role_required('admin', 'manager')
73
+ def admin_view(request):
74
+ ...
75
+ """
76
+ from functools import wraps
77
+
78
+ def decorator(view_func):
79
+ @wraps(view_func)
80
+ def wrapper(request, *args, **kwargs):
81
+ from django.http import JsonResponse
82
+
83
+ cas_user = getattr(request, 'cas_user', None) or request.session.get('cas_user')
84
+ if not cas_user:
85
+ return JsonResponse({'error': 'Authentication required'}, status=401)
86
+
87
+ user_roles = set(cas_user.get('roles', []))
88
+ if not user_roles & set(roles):
89
+ return JsonResponse({'error': 'Insufficient permissions'}, status=403)
90
+
91
+ return view_func(request, *args, **kwargs)
92
+ return wrapper
93
+ return decorator
94
+
95
+
96
+ # =============================================================================
97
+ # Flask Middleware / Decorators
98
+ # =============================================================================
99
+
100
+ def flask_cas_required(cas_client):
101
+ """
102
+ Flask decorator for CAS authentication.
103
+
104
+ Usage:
105
+ from cas_client.middleware import flask_cas_required
106
+
107
+ @app.route('/dashboard')
108
+ @flask_cas_required(cas)
109
+ def dashboard():
110
+ user = flask.session['cas_user']
111
+ return f'Hello {user["username"]}'
112
+ """
113
+ from functools import wraps
114
+
115
+ def decorator(f):
116
+ @wraps(f)
117
+ def wrapper(*args, **kwargs):
118
+ import flask
119
+
120
+ cas_user = flask.session.get('cas_user')
121
+ if cas_user:
122
+ flask.g.cas_user = cas_user
123
+ return f(*args, **kwargs)
124
+
125
+ # Check Bearer token
126
+ auth = flask.request.headers.get('Authorization', '')
127
+ if auth.startswith('Bearer '):
128
+ token = auth[7:]
129
+ user = cas_client.get_user_from_token(token)
130
+ if not user:
131
+ user = cas_client.validate_token(token)
132
+ if user:
133
+ flask.g.cas_user = user
134
+ flask.session['cas_user'] = user
135
+ return f(*args, **kwargs)
136
+
137
+ if flask.request.is_json:
138
+ return flask.jsonify({'error': 'Authentication required'}), 401
139
+ login_url = flask.current_app.config.get('CAS_LOGIN_URL', '/auth/login')
140
+ return flask.redirect(f'{login_url}?return_url={flask.request.path}')
141
+ return wrapper
142
+ return decorator
143
+
144
+
145
+ def flask_role_required(cas_client, *roles: str):
146
+ """
147
+ Flask decorator for role-based access control.
148
+
149
+ Usage:
150
+ @app.route('/admin')
151
+ @flask_cas_required(cas)
152
+ @flask_role_required(cas, 'admin')
153
+ def admin():
154
+ ...
155
+ """
156
+ from functools import wraps
157
+
158
+ def decorator(f):
159
+ @wraps(f)
160
+ def wrapper(*args, **kwargs):
161
+ import flask
162
+
163
+ cas_user = getattr(flask.g, 'cas_user', None) or flask.session.get('cas_user')
164
+ if not cas_user:
165
+ return flask.jsonify({'error': 'Authentication required'}), 401
166
+
167
+ if not cas_client.user_has_any_role(cas_user, list(roles)):
168
+ return flask.jsonify({'error': 'Insufficient permissions'}), 403
169
+
170
+ return f(*args, **kwargs)
171
+ return wrapper
172
+ return decorator
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: cas-system-client
3
+ Version: 2.0.0
4
+ Summary: Python client for CAS (Central Authentication Service) SSO integration
5
+ Home-page: https://github.com/InSol-2021/one-system
6
+ Author: CAS System
7
+ Author-email: support@innovativesolution.com.np
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/InSol-2021/one-system/tree/master/packages/python-cas-client
10
+ Project-URL: Bug Tracker, https://github.com/InSol-2021/one-system/issues
11
+ Keywords: cas sso authentication jwt single-sign-on
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: requests>=2.32.0
26
+ Provides-Extra: django
27
+ Requires-Dist: django>=4.2; extra == "django"
28
+ Provides-Extra: flask
29
+ Requires-Dist: flask>=3.0; extra == "flask"
30
+ Dynamic: author
31
+ Dynamic: author-email
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: description-content-type
35
+ Dynamic: home-page
36
+ Dynamic: keywords
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: project-url
40
+ Dynamic: provides-extra
41
+ Dynamic: requires-dist
42
+ Dynamic: requires-python
43
+ Dynamic: summary
44
+
45
+ # Python CAS Client
46
+
47
+ A Python package for seamless integration with **One System** CAS (Central Authentication Service) SSO servers. Works with Django, Flask, FastAPI, and any Python framework.
48
+
49
+ **Requirements:** Python 3.9+ and `requests >= 2.32`. The optional `[django]` extra requires Django 4.2+, and the `[flask]` extra requires Flask 3.0+.
50
+
51
+ ## Features
52
+
53
+ - 🔐 **Secure SSO Authentication** — JWT token-based authentication
54
+ - 🛡️ **HMAC Signature Validation** — Request signing with SHA-256
55
+ - 👥 **Role-Based Access Control** — Decorators for role protection
56
+ - 🐍 **Django & Flask Support** — Built-in middleware and decorators
57
+ - 📦 **Type Hints** — Full typing support for IDE autocomplete
58
+ - 🔄 **Token Caching** — In-memory cache for validated tokens
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install cas-system-client
64
+
65
+ # With Django support
66
+ pip install cas-system-client[django]
67
+
68
+ # With Flask support
69
+ pip install cas-system-client[flask]
70
+ ```
71
+
72
+ ## Quick Start
73
+
74
+ ### 1. Initialize the Client
75
+
76
+ ```python
77
+ from cas_client import CasClient
78
+
79
+ cas = CasClient(
80
+ server_url='https://your-cas-server.com',
81
+ client_id='your_client_id',
82
+ client_secret='your_client_secret',
83
+ callback_url='https://your-app.com/cas/callback',
84
+ )
85
+ ```
86
+
87
+ ### How the One System SSO flow works
88
+
89
+ 1. Send the browser to `cas.get_login_url()` — this redirects to
90
+ `GET {server_url}/sso/login?client_id=...`.
91
+ 2. One System authenticates the user and redirects back to your registered
92
+ `callback_url` with a single-use JWT appended as `?token=<JWT>`.
93
+ 3. In your callback, call `cas.validate_token(token)`. The client validates the
94
+ token **server-to-server** (sending `client_id` + `client_secret`) and returns
95
+ the user dict `{id, username, email, ...}` on success, or `None`. Tokens are
96
+ single-use — validate once, then rely on your framework session.
97
+
98
+ ### 2. Django Integration
99
+
100
+ ```python
101
+ # settings.py
102
+ import os
103
+ from cas_client import CasClient
104
+
105
+ CAS_CLIENT = CasClient(
106
+ server_url=os.environ['CAS_SERVER_URL'],
107
+ client_id=os.environ['CAS_CLIENT_ID'],
108
+ client_secret=os.environ['CAS_CLIENT_SECRET'],
109
+ callback_url=os.environ['CAS_CALLBACK_URL'],
110
+ )
111
+ CAS_PROTECTED_PATHS = ['/dashboard', '/admin']
112
+ CAS_LOGIN_URL = '/auth/login'
113
+
114
+ MIDDLEWARE = [
115
+ ...
116
+ 'cas_client.middleware.DjangoCasMiddleware',
117
+ ]
118
+
119
+ # views.py
120
+ from cas_client.middleware import django_role_required
121
+
122
+ @django_role_required('admin')
123
+ def admin_view(request):
124
+ user = request.cas_user
125
+ return JsonResponse({'user': user['username']})
126
+ ```
127
+
128
+ ### 3. Flask Integration
129
+
130
+ ```python
131
+ from cas_client import CasClient
132
+ from cas_client.middleware import flask_cas_required, flask_role_required
133
+
134
+ cas = CasClient(
135
+ server_url='https://your-cas-server.com',
136
+ client_id='your_client_id',
137
+ client_secret='your_client_secret',
138
+ )
139
+
140
+ @app.route('/dashboard')
141
+ @flask_cas_required(cas)
142
+ def dashboard():
143
+ user = flask.g.cas_user
144
+ return f'Hello {user["username"]}'
145
+
146
+ @app.route('/admin')
147
+ @flask_cas_required(cas)
148
+ @flask_role_required(cas, 'admin')
149
+ def admin():
150
+ return 'Admin area'
151
+ ```
152
+
153
+ ### 4. Manual Authentication
154
+
155
+ ```python
156
+ # Generate SSO token
157
+ token_data = cas.generate_sso_token('john_doe')
158
+
159
+ # Validate token
160
+ user = cas.validate_token(token)
161
+
162
+ # Role checks
163
+ if cas.user_has_role(user, 'admin'):
164
+ print('User is admin')
165
+
166
+ # Logout
167
+ cas.logout(token)
168
+ ```
169
+
170
+ ## API Reference
171
+
172
+ | Method | Description |
173
+ |--------|-------------|
174
+ | `get_login_url(return_url)` | Generate CAS login URL |
175
+ | `generate_sso_token(username)` | Generate SSO token |
176
+ | `validate_token(token)` | Validate token, returns user dict |
177
+ | `get_user_from_token(token)` | Get cached user data |
178
+ | `logout(token)` | Logout from CAS server |
179
+ | `user_has_role(user, role)` | Check single role |
180
+ | `user_has_any_role(user, roles)` | Check any of roles |
181
+ | `user_has_all_roles(user, roles)` | Check all roles |
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1,8 @@
1
+ cas_client/__init__.py,sha256=djdPNo6AtrY_wQ2m5-NAdaB9kRHgvuuUgLuaVVQne2k,109
2
+ cas_client/client.py,sha256=Gf-9Zv8CrwWivMv8oj4z4YgnTfuCEAQh-uPR4WLs1VY,9545
3
+ cas_client/middleware.py,sha256=E2_tuh5WpAxoe-Lr-z8ooj9q7TuX031JOelfgVqB5b8,5631
4
+ cas_system_client-2.0.0.dist-info/licenses/LICENSE,sha256=DJasJTjN70sO3GUtoYI66h5DY19DVdsDFhN-Eymwav0,1067
5
+ cas_system_client-2.0.0.dist-info/METADATA,sha256=KIBB68vEnbuPrwfPjCMu5GzyrC176bzHZwLh9S6nmS0,5461
6
+ cas_system_client-2.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ cas_system_client-2.0.0.dist-info/top_level.txt,sha256=e3BIAWc0mI-JIPNH4UvpD3EYQM3xlrYD9k0jk_kDk8s,11
8
+ cas_system_client-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 InSol-2021
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cas_client