jknife 0.0.1__py2.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.
@@ -0,0 +1,202 @@
1
+ # import packages from default or pip library
2
+ import jwt
3
+ from redis.exceptions import ConnectionError
4
+ from random import randint
5
+ from fastapi import Depends, HTTPException, status
6
+ from fastapi.security import HTTPBasic, HTTPBearer, HTTPAuthorizationCredentials
7
+ from datetime import datetime, timedelta, timezone
8
+
9
+ # import packages from this framework
10
+ from settings import AUTHENTICATION
11
+ from src.jknife.db import logger
12
+ from src.jknife.db import redis_connector
13
+
14
+
15
+ # create secret key in this file.
16
+ def create_secretkey() -> str:
17
+ return "".join([chr(randint(65, 122)) for _ in range(256)])
18
+
19
+
20
+ # SETTINGS
21
+ _DEFAULT_TOKEN_VALID_TIME: int = 20
22
+ _DEFAULT_TOKEN_REFRESH_TIME: int = 24 * 60 * 30
23
+ _TOKEN_SETTING: dict = AUTHENTICATION.get("token")
24
+ _TOKEN_SECRET_KEY: str = _TOKEN_SETTING.get("secret_key") or create_secretkey()
25
+ _TOKEN_ISSUER: str = _TOKEN_SETTING.get("token_issuer") or "127.0.0.1"
26
+ _TOKEN_AUDIENCE: str = _TOKEN_SETTING.get("token_audience") or "http://127.0.0.1"
27
+ _TOKEN_EXP_ACCESS: int = _TOKEN_SETTING.get("token_valid_time") or _DEFAULT_TOKEN_VALID_TIME
28
+ _TOKEN_EXP_REFRESH: int = _TOKEN_SETTING.get("token_refresh_time") or _DEFAULT_TOKEN_REFRESH_TIME
29
+ _TOKEN_ALGORITHM: str = _TOKEN_SETTING.get("algorithm") or "HS256"
30
+
31
+
32
+ # Bearer Token Window
33
+ oauth2_scheme = HTTPBearer(scheme_name="bearer",
34
+ description="Authentication related with JWT token.\nInput access token got after signing in.",
35
+ auto_error=False)
36
+ basic_scheme = HTTPBasic(scheme_name="basic",
37
+ description="Authentication with username and password.\nInput username and password.",
38
+ auto_error=False)
39
+
40
+
41
+ # define function for tokens
42
+ def _check_blacklist_access_token(token: str) -> str:
43
+ """
44
+ check whether the access token is registered as blacklist in redis.
45
+
46
+ :param token: access_token in string
47
+ :return: access_token, if it is not registered in redis
48
+ """
49
+
50
+ user_unique = decode_jwt_token(token=token).get("sub")
51
+
52
+ if redis_connector.get(name=f"access_token:{token}"):
53
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
54
+ detail={"msg": "invalid access token"})
55
+
56
+ elif not redis_connector.get(name=f"refresh_token:{user_unique}"):
57
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
58
+ detail={"msg": "you have to sign in first."})
59
+
60
+ return token
61
+
62
+ def check_whitelist_refresh_token(user_unique: str, token: str):
63
+ """
64
+ check whether the refresh token is registered as whitelist in redis.
65
+
66
+ :param token: refresh_token in string
67
+ :return: refresh_token, if it is registered in redis
68
+ """
69
+
70
+ registered_token: bytes = redis_connector.get(name=f"refresh_token:{user_unique}")
71
+ if registered_token is None:
72
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
73
+ detail={"msg": "you have to sign in first."})
74
+
75
+ if registered_token.decode("utf-8") != token:
76
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
77
+ detail={"msg": "invalid refresh token"})
78
+ return token
79
+
80
+ def create_jwt_token(user_unique: str, user_data: dict, is_access: bool=True) -> str:
81
+ """
82
+ create jwt token(access and refresh) with user data.
83
+
84
+ :param user_unique: primary key to distinguish specific object among the users.
85
+ :param user_data: user data that is customised by developer.
86
+ :param is_access: set token type as bool for access(default) or refresh(False).
87
+ :return: encrypted string token.
88
+ """
89
+
90
+ payload: dict = {
91
+ "iss": _TOKEN_ISSUER,
92
+ "sub": str(user_unique),
93
+ "aud": _TOKEN_AUDIENCE,
94
+ "exp": datetime.now(timezone.utc) + timedelta(minutes=_TOKEN_EXP_ACCESS if is_access else _TOKEN_EXP_REFRESH),
95
+ "user_data": user_data,
96
+ }
97
+ return jwt.encode(payload=payload,
98
+ key=_TOKEN_SECRET_KEY,
99
+ algorithm=_TOKEN_ALGORITHM)
100
+
101
+ def decode_jwt_token(token:str, is_access:bool=True) -> dict | None:
102
+ """
103
+ decode jwt token information that encrypted in token string.
104
+
105
+ :param token: token value in string
106
+ :param is_access: set token type as bool for access(default) or refresh(False).
107
+ :return:
108
+ """
109
+
110
+ try:
111
+ return jwt.decode(jwt=token,
112
+ key=_TOKEN_SECRET_KEY,
113
+ audience=_TOKEN_AUDIENCE,
114
+ algorithms=_TOKEN_ALGORITHM)
115
+
116
+ except jwt.exceptions.ExpiredSignatureError:
117
+ token_type: str = "access" if is_access else "refresh"
118
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
119
+ detail={"msg": f"{token_type} token was expired"})
120
+
121
+ except jwt.exceptions.DecodeError:
122
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
123
+ detail={"msg": "invalid access token. can not decode token information."})
124
+
125
+ def get_bearer_access_token(user_cred: HTTPAuthorizationCredentials = Depends(oauth2_scheme)) -> str:
126
+ """
127
+ get access token from HTTPBearer credential in RestAPI Docs Page.
128
+
129
+ :param user_cred: get access_token information from HTTPBearer credential
130
+ :return: return access_token
131
+ """
132
+
133
+ if user_cred is None:
134
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
135
+ detail={"msg": "missing access token in headers."})
136
+
137
+ if user_cred.scheme.lower() != "bearer":
138
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
139
+ detail={"msg": "invalid access token. token type is not match."})
140
+
141
+ return _check_blacklist_access_token(token=user_cred.credentials)
142
+
143
+ def get_token_info(token: str, is_access: bool = True) -> dict:
144
+ """
145
+ decrypt JWT token and return its data as dict type.
146
+
147
+ :param token: JWT token(access or refresh)
148
+ :param is_access: set token type as bool for access(default) or refresh(False).
149
+ :return: token information in dict type.
150
+ """
151
+
152
+
153
+ payload: dict = decode_jwt_token(token=token, is_access=is_access)
154
+ return payload
155
+
156
+ def register_blacklist_access_token(token: str, ex: int=_TOKEN_EXP_ACCESS) -> None:
157
+ """
158
+ register JWT access token in redis after signout or changing password
159
+ template: access_token: {ACCESS_TOKEN}
160
+
161
+ :param token: access_token that will be registered in redis.
162
+ :param ex: token expiration time in second.
163
+ :return: None
164
+ """
165
+
166
+ try:
167
+ redis_connector.set(name=f"access_token:{token}", value="1", ex=ex * 60)
168
+ except ConnectionError:
169
+ logger.warning(msg="fail to register access_token as a blacklist. can not connect to redis db.")
170
+
171
+ return None
172
+
173
+ def register_whitelist_refresh_token(user_unique: str, token: str) -> None:
174
+ """
175
+ register JWT refresh token in redis after signin.
176
+ template: refresh_token:{USER_UNIQUE}
177
+
178
+ :param user_unique: primary key for each user.
179
+ :param token: refresh token that was issued after signin.
180
+ :return: None
181
+ """
182
+ try:
183
+ redis_connector.set(name=f"refresh_token:{user_unique}", value=token, ex=_TOKEN_EXP_REFRESH * 60)
184
+ except ConnectionError:
185
+ logger.warning(msg="fail to register refresh_token as a whitelist. can not connect to redis db.")
186
+
187
+ return None
188
+
189
+ def remove_whitelist_refresh_token(user_unique: str) -> None:
190
+ """
191
+ remove registered refresh token in redis after signout or changing password.
192
+
193
+ :param user_unique: primary key for each user.
194
+ :return: None
195
+ """
196
+
197
+ try:
198
+ redis_connector.delete(f"refresh_token:{user_unique}")
199
+ except ConnectionError:
200
+ logger.warning(msg="fail to remove refresh token from redis. can not connect to redis db.")
201
+
202
+ return None
@@ -0,0 +1,76 @@
1
+ # import packages from default or pip library
2
+ from fastapi.exceptions import RequestValidationError
3
+
4
+ # import packages from this framework
5
+ from settings import PASSWORD_POLICIES
6
+
7
+
8
+ # settings
9
+ MIN_LENGTH: int = PASSWORD_POLICIES.get("min_length")
10
+ SPECIAL_CHARS: str = "!@#$%^&*()_-+=~`\'\";:\\|<>,.?/"
11
+
12
+
13
+ # define password policy functions
14
+ def password_minlen(password: str, field: str = "password"):
15
+ if len(password) >= MIN_LENGTH:
16
+ return password
17
+
18
+ raise RequestValidationError(errors={"input": field,
19
+ "msg": f"{field} must be at least 8 chars"})
20
+
21
+
22
+ def password_upchar(password: str, field: str = "password") -> str:
23
+ for c in password:
24
+ if c.isupper():
25
+ return password
26
+
27
+ raise RequestValidationError(errors={"input": field,
28
+ "msg": f"{field} must contain at least one upper character"})
29
+
30
+ def password_lowchar(password: str, field: str = "password") -> str:
31
+ for c in password:
32
+ if c.islower():
33
+ return password
34
+
35
+ raise RequestValidationError(errors={"input": field,
36
+ "msg": f"{field} must contain at least one lower character"})
37
+
38
+
39
+ def password_number(password: str, field: str = "password") -> str:
40
+ for c in password:
41
+ if c.isnumeric():
42
+ return password
43
+
44
+ raise RequestValidationError(errors={"input": field,
45
+ "msg": f"{field} must contain at least one number"})
46
+
47
+
48
+ def password_special(password: str, field: str = "password") -> str:
49
+ for c in password:
50
+ if c in SPECIAL_CHARS:
51
+ return password
52
+
53
+ raise RequestValidationError(errors={"input":field,
54
+ "msg":f"{field} must contain at least one special character"})
55
+
56
+
57
+ # define dependencies functions
58
+ def validate_password_policy(password: str, field: str = "password") -> str:
59
+ policy_mapping: dict = {
60
+ "PASSWORD_MINLEN": password_minlen,
61
+ "PASSWORD_UPCHAR": password_upchar,
62
+ "PASSWORD_LOWCHAR": password_lowchar,
63
+ "PASSWORD_NUMBER": password_number,
64
+ "PASSWORD_SPECIAL": password_special,
65
+ }
66
+ validate_list: list = PASSWORD_POLICIES.get("compliance")
67
+
68
+ if len(validate_list) == 0:
69
+ return password
70
+
71
+ for func in validate_list:
72
+ tmp = policy_mapping.get(func)
73
+ if tmp is not None and callable(tmp):
74
+ tmp(password=password, field=field)
75
+
76
+ return password
jknife/logging.py ADDED
@@ -0,0 +1,69 @@
1
+ import logging
2
+ import logging.config
3
+ from os import mkdir, listdir
4
+ from typing_extensions import Annotated, Doc
5
+ from settings import LOG_SETTINGS, DEBUG_MODE
6
+
7
+
8
+ # create logs folder to store log files.
9
+ if "logs" not in listdir(""):
10
+ mkdir("logs")
11
+
12
+ # set logging config from settings.py
13
+ # apply default key:value for dictConfig
14
+ LOG_SETTINGS.update({"version": 1, "disable_existing_loggers": False})
15
+ logging.config.dictConfig(config=LOG_SETTINGS)
16
+
17
+
18
+ # define LoggerMgmt for multiple logging
19
+ class LoggerMgmt:
20
+ """
21
+ This class is designed to implement multiple logging system in FastAPI.
22
+
23
+ :param logger_names: assign the logger name that you want to use in specific area.
24
+ you can define it in settings.py with LOG_SETTINGS and *_LOGGER_LIST
25
+ """
26
+
27
+ def __init__(self,
28
+ logger_names: Annotated[list[str],
29
+ Doc("assign the name of loggers to record activities")]):
30
+ self.__loggers = [logging.getLogger(name=name) for name in logger_names]
31
+ if DEBUG_MODE:
32
+ uvicorn_logger = logging.getLogger(name="uvicorn")
33
+ uvicorn_logger.setLevel(level="DEBUG")
34
+ self.__loggers.append(uvicorn_logger)
35
+
36
+ def critical(self, msg: Annotated[str,
37
+ Doc("log message for critical level")]) -> None:
38
+ for logger in self.__loggers:
39
+ logger.critical(msg=msg)
40
+
41
+ return None
42
+
43
+ def error(self, msg: Annotated[str,
44
+ Doc("log message for error level")]) -> None:
45
+ for logger in self.__loggers:
46
+ logger.error(msg=msg)
47
+
48
+ return None
49
+
50
+ def warning(self, msg: Annotated[str,
51
+ Doc("log message for warning level")]) -> None:
52
+ for logger in self.__loggers:
53
+ logger.warning(msg=msg)
54
+
55
+ return None
56
+
57
+ def info(self, msg: Annotated[str,
58
+ Doc("log message for info level")]) -> None:
59
+ for logger in self.__loggers:
60
+ logger.info(msg=msg)
61
+
62
+ return None
63
+
64
+ def debug(self, msg: Annotated[str,
65
+ Doc("log message for debug level")]) -> None:
66
+ for logger in self.__loggers:
67
+ logger.debug(msg=msg)
68
+
69
+ return None
@@ -0,0 +1,23 @@
1
+ # import packages from default or pip library
2
+ from datetime import datetime
3
+ from pydantic import BaseModel
4
+ from typing_extensions import Annotated, Doc, Optional
5
+ from uuid import UUID
6
+
7
+ # import packages from this framework
8
+
9
+
10
+ # define mixin class
11
+ class IdViewMixin(BaseModel):
12
+ id: Annotated[int,
13
+ Doc("Default integer id for each table row.")]
14
+
15
+
16
+ class UUIDViewMixin(BaseModel):
17
+ id: Annotated[UUID,
18
+ Doc("UUID format id for each table row.")]
19
+
20
+
21
+ class RegisterDateTimeViewMixin(BaseModel):
22
+ register_dt: Annotated[Optional[datetime],
23
+ Doc("Datetime that the row was added at.")]
@@ -0,0 +1,38 @@
1
+ # import packages from default or pip library
2
+ from typing_extensions import Annotated, Doc
3
+ from pydantic import BaseModel
4
+
5
+ # import packages from this framework
6
+
7
+
8
+ # Error Mixin Class
9
+ class ErrMsgMixin(BaseModel):
10
+ msg: Annotated[str,
11
+ Doc("message for error")]
12
+
13
+
14
+ class ErrMsgTypeMixin(BaseModel):
15
+ type: Annotated[str,
16
+ Doc("customised message type. for example, 'password_mismatch' or 'unauthorised access'.")]
17
+
18
+
19
+ class ErrMsgBoolResultMixin(BaseModel):
20
+ result: Annotated[bool, Doc("result of calling API in bool type.")] = False
21
+
22
+
23
+ class FieldValidationErrMixin(BaseModel):
24
+ field: Annotated[str,
25
+ Doc("field name which has an error.")]
26
+
27
+
28
+ # Error Model
29
+ class DefaultErrorMsgView(ErrMsgMixin):
30
+ pass
31
+
32
+
33
+ class ErrMsgWithTypeView(ErrMsgTypeMixin, DefaultErrorMsgView):
34
+ pass
35
+
36
+
37
+ class ErrMsgWithTypeAndResultView(ErrMsgBoolResultMixin, ErrMsgWithTypeView):
38
+ pass
@@ -0,0 +1,42 @@
1
+ # import packages from default or pip library
2
+ from datetime import date
3
+ from typing_extensions import Annotated, Doc
4
+ from pydantic import BaseModel, EmailStr
5
+
6
+ # import packages from this framework
7
+
8
+
9
+ # define mixin class
10
+ class BirthdateViewMixin(BaseModel):
11
+ birthdate: Annotated[date,
12
+ Doc("user's birthdate")]
13
+
14
+
15
+ class CPNumViewMixin(BaseModel):
16
+ cp_num: Annotated[str,
17
+ Doc("cellphone number.")]
18
+
19
+
20
+ class EmailInputViewMixin(BaseModel):
21
+ email: Annotated[EmailStr,
22
+ Doc("email address for user. it can be replaced username.")]
23
+
24
+
25
+ class FirstNameViewMixin(BaseModel):
26
+ firstname: Annotated[str,
27
+ Doc("firstname of user")]
28
+
29
+
30
+ class LastNameViewMixin(BaseModel):
31
+ lastname: Annotated[str,
32
+ Doc("lastname of user")]
33
+
34
+
35
+ class NationViewMixin(BaseModel):
36
+ nation: Annotated[str,
37
+ Doc("the nation that user came from.")]
38
+
39
+
40
+ class PostalCodeViewMixin(BaseModel):
41
+ postal_code: Annotated[str,
42
+ Doc("postal number of address.")]
jknife/views/tokens.py ADDED
@@ -0,0 +1,22 @@
1
+ # import packages from default or pip library
2
+ from typing_extensions import Annotated, Doc
3
+ from pydantic import BaseModel
4
+
5
+
6
+ # import packages from this framework
7
+
8
+
9
+
10
+ # define Class for Common SQLModel
11
+ class DefaultJWTTokenView(BaseModel):
12
+ """
13
+ This class is a view template for returning JWT tokens information.
14
+ Read Only.
15
+ """
16
+
17
+ access_token: Annotated[str,
18
+ Doc("This is an access token string for authenticated user")]
19
+ refresh_token: Annotated[str,
20
+ Doc("This is a token string for reissuing access token")]
21
+ token_type: Annotated[str,
22
+ Doc("Token type that used in this class. Default is 'Bearer'")] = "Bearer"
jknife/views/users.py ADDED
@@ -0,0 +1,70 @@
1
+ # import packages from default or pip library
2
+ from datetime import datetime
3
+ from typing_extensions import Annotated, Doc
4
+ from pydantic import BaseModel, field_validator
5
+
6
+ # import packages from this framework
7
+ from src.jknife.views.personnel_info import EmailInputViewMixin
8
+ from src.jknife.dependencies.users import validate_password_policy
9
+
10
+
11
+ # define mixin class
12
+ class UsernameInputViewMixin(BaseModel):
13
+ username: Annotated[str,
14
+ Doc("username")]
15
+
16
+
17
+ class InputPasswordViewMixin(BaseModel):
18
+ password: Annotated[str,
19
+ Doc("password for user")]
20
+
21
+
22
+ class LastSigninDateTimeViewMixin(BaseModel):
23
+ last_signin_dt: Annotated[datetime | None,
24
+ Doc("User's last signin datetime.")] = None
25
+
26
+
27
+ class SigninFailMixin(BaseModel):
28
+ is_active: Annotated[bool,
29
+ Doc("show whether the user is activated or not.")]
30
+ signin_fail: Annotated[int,
31
+ Doc("If the user fail to login, this value will be incremented.")]
32
+
33
+
34
+ # define common view class
35
+ class ChangePasswordView(InputPasswordViewMixin):
36
+ new_password: Annotated[str,
37
+ Doc("New password for changing password")]
38
+
39
+ @field_validator('new_password', mode='before')
40
+ @classmethod
41
+ def check_new_password(cls, value) -> str:
42
+ return validate_password_policy(password=value, field="new_password")
43
+
44
+
45
+ class DefaultSignupView(InputPasswordViewMixin, UsernameInputViewMixin):
46
+ password: Annotated[str,
47
+ Doc("password for user")]
48
+
49
+ @field_validator('password', mode='before')
50
+ @classmethod
51
+ def check_password(cls, value) -> str:
52
+ return validate_password_policy(password=value)
53
+
54
+
55
+ class DefaultEmailSignupView(InputPasswordViewMixin, EmailInputViewMixin):
56
+ password: Annotated[str,
57
+ Doc("password for user")]
58
+
59
+ @field_validator('password', mode='before')
60
+ @classmethod
61
+ def check_password(cls, value) -> str:
62
+ return validate_password_policy(password=value)
63
+
64
+
65
+ class UsernameSigninView(InputPasswordViewMixin, UsernameInputViewMixin):
66
+ pass
67
+
68
+
69
+ class EmailSigninView(InputPasswordViewMixin, EmailInputViewMixin):
70
+ pass
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: jknife
3
+ Version: 0.0.1
4
+ Summary: Custom FastAPI for luna-negra
5
+ Author: luna-negra
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: Backend,Customised,FastAPI,Python,pypi
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Dist: blinker>=1.9.0
12
+ Requires-Dist: fastapi[standard]>=0.166.1
13
+ Requires-Dist: mongoengine
14
+ Requires-Dist: pycountry>=24.6.1
15
+ Requires-Dist: pyjwt>=2.10.1
16
+ Requires-Dist: redis>=6.4.0
17
+ Requires-Dist: sqlmodel>=0.0.24
@@ -0,0 +1,31 @@
1
+ jknife/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ jknife/logging.py,sha256=0MVIdLEYE2hpaRHhjbS20hFKIFXVJVVDzqyJmUU8_HQ,2314
3
+ jknife/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ jknife/commands/jknife.py,sha256=Mu-oiYMjdHT0rD3jKjum6_9K_RjbxuqB2C7gr-_Qukc,11635
5
+ jknife/db/__init__.py,sha256=weR-h-z2xd4wW64kc5rCrG7zz3toe-bwggoBka_g0nA,6245
6
+ jknife/db/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ jknife/db/models/mongo/__init__.py,sha256=bDj6rFEtNudrbtPbk4iORlWwXyMv6V_PCeixyGufxP4,1984
8
+ jknife/db/models/mongo/network.py,sha256=MfFq05k_42uw4mMfcd62K7MOhuk0E5OuqfxmHD_ONR0,1106
9
+ jknife/db/models/mongo/personnel_info.py,sha256=wIalbvVI1bqCTOsShrSvy29I6OrdqaY6nYGhSxRFTf0,3273
10
+ jknife/db/models/mongo/settings.py,sha256=mFJQAAeoyWEY1J6y5_l1p4JevOYHyX27wL5A-gO35bg,2219
11
+ jknife/db/models/mongo/token.py,sha256=Xfw6LlAZ8Qu2iA9vYxyTk52aq4KoMLZ9P0KBRDiOBLI,2410
12
+ jknife/db/models/mongo/users.py,sha256=xTlkK5BiF5oSaeFyF1aTh7JDpu2K6Jr0MI2ALt0BoSA,2931
13
+ jknife/db/models/rdbms/__init__.py,sha256=0r22xz8J_kENMiPNVr_t3P5iILGXsll38wXXwnREABc,1847
14
+ jknife/db/models/rdbms/network.py,sha256=lk3p4UBbsnPRuwr5Q4CGXDL9bSAlZAEaUz-GVfEWc0U,1040
15
+ jknife/db/models/rdbms/personnel_info.py,sha256=OBwMvg8K_3XqKSSaeJzDir_pIIHNd3cxdL-2IsQFuFQ,2976
16
+ jknife/db/models/rdbms/settings.py,sha256=p_typKsxO6aCh-HNL_vlj9GIT7gm-eYkyA-vvo0wsmY,1520
17
+ jknife/db/models/rdbms/token.py,sha256=4JP3sdspM-nsBXM9v1zbM2JqI56FzKPsP7nNowZDPGo,2179
18
+ jknife/db/models/rdbms/users.py,sha256=5vldUYl3pGZjWjuWGvn6M0SeHvj869omcMS6TkBVSa4,3189
19
+ jknife/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ jknife/dependencies/token.py,sha256=uBQ_zIU56HBOZH_mQWwdQeEFv7VXdKAJn7MAFqWODsU,7765
21
+ jknife/dependencies/users.py,sha256=G6bJ-vHz7hlHXn2OpNEAoVB1H8DJhwc4XBMBmKW1v6Q,2502
22
+ jknife/views/__init__.py,sha256=uXUiBzToN6gmb7zcHUyPbniNjCtzbyN31FrIVs7PpI0,596
23
+ jknife/views/error_message.py,sha256=g3xS8Z0R1BeP9XpmETgLirDdghM789iGXBfU-K8wwWM,887
24
+ jknife/views/personnel_info.py,sha256=JL_GqerZjhgs4AJQsQ2eeUZxCh17G8zXuWsDqaEI2tk,957
25
+ jknife/views/tokens.py,sha256=PJh_fGEwK3T6Kq0aY-CuJPWCTdcHFrCSd2NhJMZqYqU,647
26
+ jknife/views/users.py,sha256=ndbSTMlEeiwazfejHrGj37Jnw-e40Y6YoZwzyyyM4ns,2041
27
+ jknife-0.0.1.dist-info/METADATA,sha256=VPDim94DKvnv8t2T4RQrpcWOaOcBP7j1aRoQ6tWGa9I,519
28
+ jknife-0.0.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
29
+ jknife-0.0.1.dist-info/entry_points.txt,sha256=08tkSuEw5jKr7Jz0cFlcKr8b2fjsQIf4Mzmb0cB8XOM,55
30
+ jknife-0.0.1.dist-info/licenses/LICENSE,sha256=myY0xrsHJcytQadTeGVqmIo6QlskAAf5epHL-WRQe30,1067
31
+ jknife-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jknife = jknife.commands.jknife:main
@@ -0,0 +1,7 @@
1
+ Copyright <YEAR> <COPYRIGHT HOLDER>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.