diadia

興味があることをやってみる。自分のメモを残しておきます。

セッターゲッターについて

まずアクセス修飾子について

アクセス修飾子はクラスを基準としてアクセスできるかどうかを定めるものと考えると良い。

class Person{
    public name: string;

    constructor(name: string){
        this.name = name;
    };
}
let mario = Person("mario");
mario.name = 'maria';

console.log(mario.name);
// maria

クラス外でもnameプロパティにアクセスできるのがpublicで、これをクラス外からアクセスできないようにするにはprivateを使う。

class Person{
    private name: string;

    constructor(name: string){
        this.name = name;
    };
}
let mario = Person("mario");

console.log(mario.name);
// エラー クラス外からプロパティ値を参照できない

mario.name = 'maria';
// エラー クラス外からプロパティを変えられない

privateを使うことで疎結合できるメリットが有るってことらしい。ここはおいておく。 疎結合できることをメリットとすると、クラス外からアクセスする事ができない。

じゃあ特別に参照できるようにするわけで、その時にゲッターを使う。

class Person{
    private name: string;

    constructor(name: string){
        this.name = name;
    };

    get name(){
        return this.name
    };
}
let mario = Person("mario");

console.log(mario.name);
// mario

mario.name = 'maria';
// エラー クラス外からプロパティを変えられない

プロパティの値を変えたいって思うならセッターを使うことになる。ここで疑問。セッターを使えるようにしたら、それはpublicと同じではないか、と。
セッターを使う意義について考えてみたけど、例えばクラス外から参照はできないけどプロパティ属性を変える事ができる場合につかったり、クラス外から属性値をセットする際に不適切値だったらエラーを出すってことしか考えられない。このへんはなにかわかったら書き加えたいところだ。

class Person{
    private _name: string;

    constructor(name: string){
        this._name = name;
    };

    get name(){
        return this._name
    };

    set name(value:string){
        if (!value || value === '') { 
            throw new Error('値が不適切です');    
        }
        this._name = value;
    }
}
let mario = Person("mario");

console.log(mario.name);
// mario

mario.name = 'maria';

あとgetだけでsetがない場合はreadonlyに自動的になるみたい。

typescriptコンパイラの設定について

udemyの超TypeScript入門 完全パック(2021)の内容をメモしている。とてもわかり易いのでおすすめ。

コンパイラの設定方法

  1. watchモードを使って、保存時に自動的にTSからJSにコンパイルする方法
  2. tsc --initコマンドでtsconfig.jsonを作り、すべてのファイルを一気にコンパイルする方法
  3. include,exclude,filesを使ってコンパイルするファイルを選ぶ方法
  4. targetを指定して、特定のバージョンのjavascriptに変換する方法
  5. libを指定して、Typescriptが用意している型の定義を追加する
  6. allowJs, checkJs, jsx, declaration, declarationMapの設定方法
  7. SourceMapを使用して、ブラウザでTypescriptを操作する方法
  8. outDirとrootDir,, removeDir, removeComments, noEmit, downlevelLeterationの使い方
  9. noEmitIbErorオプションを使って、エラーが出た時にコンパイルしない方法
  10. noImplicitAnyやstrictnullChecksなどのstrictの設定方法
  11. きれいなコードを書くための設定方法
コンパイラの設定のドキュメント

TypeScript: Documentation - tsc CLI Options

memo

1. watch モードを使って、保存時に自動的に TS から JS にコンパイルする方法

watch モードを使わないでコンパイルする方法は、以下のコマンド使ってコンパイルする。

// tscコマンドを使う
tsc index.js
// tsc {コンパイルしたいファイル}

watch モードを使う

// -w または --watchオプションを使う
tsc -w
tsc --watch
// 止める際は ctl + C

2. tsc --init コマンドで tsconfig.json を作り、すべてのファイルを一気にコンパイルする方法

tsc コマンドでは、コンパイル対象のファイルを指定する必要があるが、それを予め設定することでファイル指定する手間を省く事ができる。その手間を省くには、tsconfig.json を作っておく必要がある。tsconfig.json は手動で作成するのではなく、以下のコマンドで作成する。

// tsconfig.jsonファイルを作成する
tsc --init

djangoのインテリセンスを有効化する

参考: https://kic-yuuki.hatenablog.com/entry/vscode-python-autocomplete

この方の記事が分かりやすかった。 djangovscodeで書く際にインテリセンスを働かせる。

visual studio code のルートディレクトリ(プロジェクト)に.vscode/settings.jsonを作成する。そしてsettings.json内にインテリセンスを働かせたいライブラリのパスを追加するとインテリセンスがきくようになる。

.vscode/settings.jsonを作成

mkdir project 
cd project
mkdir .vscode
vi settings.json

settings.jsonで書く内容

{
  "python.autoComplete.extraPaths": [
    "ライブラリが格納されるパス",
  ]
}

ライブラリが格納されるパスは以下で確認することができる。

>> python

>>import django
>>django.path
['/Users/chiaki/.local/share/virtualenvs/django_test_sample-9pz0Y4Xg/lib/python3.9/site-packages/django']

# ライブラリが格納されるパスはsite-packagesまでなので
# '/Users/chiaki/.local/share/virtualenvs/django_test_sample-9pz0Y4Xg/lib/python3.9/site-packages/'

これをsettings.jsonに書くと以下のようになる。

{
    "python.autoComplete.extraPaths": [
        "/Users/chiaki/.local/share/virtualenvs/django_test_sample-9pz0Y4Xg/lib/python3.9/site-packages/"
    ]
}

typescriptのメモ

typescriptの特徴

Detecting errors in code without running it is referred to as static checking. Determining what’s an error and what’s not based on the kinds of values being operated on is known as static type checking. TypeScript checks a program for errors before execution, and does so based on the kinds of values, it’s a static type checker. For example, the last example above has an error because of the type of obj.

TypeScript: Documentation - TypeScript for the New Programmer

typescriptの特徴はコードを走らせる前からエラーを発見する事ができる。エラーは値の種類に基づいてエラーを発見する。

Roughly speaking, once TypeScript’s compiler is done with checking your code, it erases the types to produce the resulting “compiled” code. This means that once your code is compiled, the resulting plain JS code has no type information.

TypeScript: Documentation - TypeScript for the New Programmer

typescriptはコンパイルされると型を消失し、型のないjavascriptに変化する。

We frequently see the question “Should I learn JavaScript or TypeScript?“. The answer is that you can’t learn TypeScript without learning JavaScript! TypeScript shares syntax and runtime behavior with JavaScript, so anything you learn about JavaScript is helping you learn TypeScript at the same time.

TypeScript: Documentation - TypeScript for the New Programmer

javascriptを学べば、typescriptに直結する。

typescriptのインストール方法

npm でインストールすれば良い

>> npm install -g typescript

typescriptをjavascriptに変換する方法

typescriptをインストールすると、tscコマンドが使えるようになる。tscはtypescriptcompileってことでjavascriptに変換するコマンドである。 このコマンドを使うとコンパイル対象のファイルが新たにjsバージョンで作成される。

>> tsc コンパイル対象の.tsファイル

typescript

型推論ができないときっていつなのか知りたい。 一例:

let test;
// この場合testに値が入ってないので型推論しようとしてもできない。

typescriptの型

// typescriptの型とjavascriptの型は同じ名前の型であっても全く別物と考えること

let hasValue: boolean = true;
let count: number = 10;
let float: number = 5.21;
let negative: number = -3.522;
let single: string = 'hello';
let double: string = "hello";
let back: string = `hello`;

// オブジェクトに型をつける場合
const person: {
    name: string;
    age: number;
} = {
    name: 'Jack',
    age: 21
}

// 配列に型をつける
const fruits: string[] = ['Apple', 'Banana', 'Grape']


// tuple型を使って決まった内容の配列を作る
const book: [string, number, boolean] = ['business', 1500, false];
//このように書くと第一要素はstring,第2要素はnumber...と型を指定した配列を定義する事ができる。


//列挙型をENUMの使って定義する
enum CoffeeSize  {
    SHORT = 'SHORT',
    TALL = 'TALL',
    GRANDE = "GRANDE",
    VENTI = 'VENTI'        
}
//こういう書き方もある
enum CoffeeSize1  {
    SHORT,
    TALL,
    GRANDE,
    VENTI     
}



// union -> 数字も文字列も入れたいときに使う
let unionType: number | string = 10;
unionType.toUpperCase(); // エラー
unionType = `hello`;
unionType.toUpperCase(); // エラーが出ない

// unionの配列を作りたい場合
let unionTypes: (number | string)[] = [10, 'hello'];


// Literal型(特定の決まった値のみを扱う)
let apple: 'apple' = 'apple'; // type'apple'で、'apple'しか入れることができない
apple = 'hello'; // エラー 'apple'のみ受け入れないからエラー


// typeエイリアス
type ClothSize = 'small' | 'medium' | 'large';
let clothSize: ClothSize = "large";
// これは以下のことと同じ
let clothSize1: 'small' | 'medium' | 'large' = 'large';


// 関数
function add(num1: number, num2: number): number { 
    return num1 + num2
}

function sayHello(): void {
    console.log('hello');
}

// callback関数を使う場合
function doubleAndhandle(num: number, cb: (num: number) => number): void {
    const doubleNum = cb(num * 2);

    console.log(num * 2);
}
doubleAndhandle(21, doubleNum => {
    return doubleNum
});

コンパイラの設定方法について

typescriptコンパイラの設定について - diadia

ゲッター、セッターについて

セッターゲッターについて - diadia

インターフェースについて

インターフェース = オブジェクトのみのタイプエイリアス

インターフェイスの定義

interface Human {
    name: string;
    age: number;
}


const developer: Human = {
    name: 'Tom',
    age: 25
}

implements を使用するとクラスに対してインターフェイスを利用する事ができる

class Developer implements Human {
  constructor(public name: string, public age: number);
  greeting(message: string): void {
    return `${message}`;
  }
}

カスタムディレクティブを使う

ドキュメント

カスタムディレクティブ — Vue.js

作り方概要

以下2つのステップで実装する。
カスタムディレクティブを定義し、カスタムディレクティブを適用したい要素に設定する。

カスタムディレクティブを定義する

カスタムディレクティブを定義する方法はグローバルとローカルの2つの方法がある。 グローバルでカスタムディレクトリを使いたい場合はmain.jsで定義する。ローカルでカスタムディレクティブを使いたい場合、すなわちそのコンポーネントのみでカスタムディレクティブを使いたい場合は、当該コンポーネントで定義する。

グローバルで定義する

//main.js 

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// カスタムディレクティブをグローバルで使うときはVue.directiveを使う
// 引数は2つ取る 名前とオブジェクトである
Vue.directive('border', {
  bind(el, binding, vnode){},
  update(el, binding, vnode){}
});

new Vue({
  render: h => h(App),
}).$mount('#app')

ローカルで定義する

// border という名前のカスタムディレクティブを登録

new Vue({
  el: 'app',
  directives: {
    border: {
      bind(el, binding, vnode){},
      update(el, binding, vnode){}
    }
  }
})

フック関数

カスタムディレクティブはVueのようなライフサイクルを持っていて、カスタムディレクティブの挙動を定める=フック関数の挙動を編集する、と置き換えられる。このフック関数はパラメータを取ることができる。それはel, binding, vnode, oldnodeである。 elはDOM操作をしたいときに使い要素だと考えれば良い。
bindingはカスタムディレクティブにセットされた属性値を取り出すときに使われる。

フック関数の種類
  • bind()...ディレクティブが初めて対象の要素に紐付いたとき
  • inserted()... 親Noneに挿入されたとき
  • update()... コンポーネントが更新されるたび。子コンポーネントが更新されるたび
  • componentUpdated()...コンポーネントが更新されるたび。子コンポーネントが更新された後
  • unbind()... ディレクティブが紐付いてる要素から取り除かれたとき
elを使ったサンプル例
// border という名前のカスタムディレクティブを登録

new Vue({
  el: 'app',
  directives: {
    border: {
      bind(el, binding, vnode){
          el.textContent = 'カスタムディレクティブが働いていればこの文章が見れる';
      },
      update(el, binding, vnode){}
    }
  }
})
bindingを使ったサンプル例

bindingはカスタムディレクティブにセットされた属性値を取り出すときに使われる。てことで、カスタムディレクティブを定義するだけでなく、カスタムディレクティブを使う場面で属性値を定める必要もある

// border という名前のカスタムディレクティブを登録

new Vue({
  el: 'app',
  directives: {
    border: {
      bind(el, binding, vnode){
          // bindingを使ってカスタムディレクティブの属性値を取り出す
          // value変数は'カスタムディレクティブが働いていればこの文章が見れる'が入っている
          const value = binding.value;
          el.textContent = value;
      },
      update(el, binding, vnode){}
    }
  }
})
// HelloWorld.vue
<template>
<p v-border="'カスタムディレクティブが働いていればこの文章が見れる'">Home</p>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

その他

基本的にbindとupdateを使い、bindとupdateは同じ動作を書くのが基本らしい。

カスタムディレクティブを使うどころ。 -> コードを抽象化したいときに使う!!

カスタムディレクティブを適用したい要素に設定

すべてのカスタムディレクティブはvから始まる。
カスタムディレクティブはv-hogehogeと名付けられる。定義する際はそのv-を除いて定義する。

以下はカスタムディレクティブの呼び出し(定義したものを使用)を指している。 カスタムディレクティブを定義するのは、main.jsかコンポーネントである。

// HelloWorld.vue
<template>
<p v-border>Home</p> # コレ
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

javascript DOM操作でcssを変更する

DOM操作の一つとしてcssを変更する。

pタグのcssのborderを"solid black 2px"に変更したい場合は要素のstyleプロパティにアクセスして変更すれば良い。

てことで、要素の取得する。その要素のプロパティを変更する。このステップを踏めば良い。

<!-- html -->
<p id='practice'>styleを変更したい</p>
// javascript
var ele = document.getElementById("practice");
ele.style.border = "solid black 2px";

vueのプロジェクト始め方(vue-cliを使って)

1. Vue cliをインストールする

npm install -g @vue/cli
vue --version

2. 新規プロジェクトの作成

#vue create プロジェクト名
vue create myproject

プロジェクトを作る際に出てくるbabel, vuexについて - babelはトランスパイラ。 - Vuexは状態管理を行うためのもの - Routerルート管理するためのもの

3. 作成したプロジェクトをrun server する

cd myproject
npm run serve

SQLITE3のINSERT文を動的に作成する。

sqlite3のインサート文は文字列である必要がある。 文字列であるがゆえにダイナミックなインサートは実行しづらいが、 辞書型のデータをもとにインサート文を作成する事ができる。また同一の辞書型データからインサートの値であるタプルを作成できる。
だから動的なものを作るには辞書型のデータを作成すると作れるって覚えておくと便利。

import sqlite3


class JobOffer(object):
    id = None  # int
    company_name = None  # str
    age = None  # str
    category = None  # str
    type = None  # str
    position = None  # str
    license = None  # str
    period = None  # str
    sector = None  # str
    
    def __init__(self, data):
        # dataはプロパティ名とプロパティの値が格納されているものとする
        for key, value in data.items():
            setattr(self, key, value)
        print('attrチェック', self.__dict__)

    def generate_insert_data(self):
        # insertに使うデータはインスタンスのプロパティなのでデータを渡す必要はない
        data = {}
        data['table'] = 'my_table'
        values = {}
        for key, value in self.__dict__.items():
            values[key] = value
        data['values'] = values
        return data

    def create(self, insert_data):
        """ insert 例:cur.execute("INSERT INTO mydb VALUES (?, ?)", (a,b)) """
        table = insert_data['table']
        columns = ', '.join(insert_data["values"].keys())
        hatena = ', '.join(['?' for n in insert_data["values"].values()])
        #print(f"INSERT INTO {table} ({columns}) VALUES ({hatena})" + "\n")
        insert_sql = f"INSERT INTO {table} ({columns}) VALUES ({hatena})"
        data = tuple(insert_data['values'].values())
        # print(data)
        conn = sqlite3.connect('db.sqlite3')
        cursor = conn.cursor()
        try:
            cursor.execute(insert_sql, data)
            conn.commit()
        except sqlite3.IntegrityError as e:
            print('UNIQUE constraint failed')
        cursor.close()
        conn.close()

    def update(self):
        pass

    @staticmethod
    def create_table():
        conn = sqlite3.connect('db.sqlite3')
        cursor = conn.cursor()
        create_table_sql = """CREATE TABLE my_table (
            id integer primary key autoincrement, company TEXT,
            age TEXT, category TEXT, type TEXT,
            position TEXT, license TEXT,
            period TEXT, sector TEXT )"""
        cursor.execute(create_table_sql)



Seleniumを使う際のメモ(改訂版)

困ったときの参照ページ

Python + Selenium で Chrome の自動操作を一通り Selenium の API (Official) Selenium with Python

使用準備

まずseleniumを扱う環境を作成した。anacondaを利用して環境を設定した。

conda create -n selenium python=3.6.4
pip install selenium

これだけではseleniumを利用することはできず、実際にブラウザを制御するドライバーも必要になる。windowsでの環境設定を試みたが、うまく行かなかったのでmacで行った。windowsについては最後まで環境構築できたら記録する。 ドライバーのダウンロード先:https://www.seleniumhq.org/download/ ダウンロードページの少しスクロールした先にThird Party Browser Drivers NOT DEVELOPED by seleniumhqがあり、Google Chrome Driverがある。これをクリックしてバージョンを指定するとmac用のソースがあるのでダウンロードする。

簡単なスクレイプコード

今回はwebdriverを呼び出して、特定のページを表示させ、そのページの特定のタグをスクレイピングするコードを書く。

python

from selenium import webdriver

driver = webdriver.Chrome("User/myusername/desktop/chromedriver")
#Chrorme内にはダウンロードしたwebdriverのフルパスを記述すること 
driver.get("https://yahoo.co.jp/")
for h2 in driver.find_elements_tag_name("h2")
    print(h2)

classで取得する

# class属性値であり.classNameと書く必要はない
driver.find_element_by_tag_name("className")

入力フォームに入力する

入力フォーム(inputタグ)を要素として取得する。そして取得した要素に対し、入力したいキーワードをsend_keys()を使って入力する。

from selenium import webdriver
driver = webdriver.Chrome("/users/myusername/desktop/chromedriver")
driver.get("https://google.co.jp")
input = driver.find_element_by_id("lst-ib")
#googleの検索フォームid名はlst-idである。
input.send_keys("ヤフーニュース")

seach_button = driver.find_element_by_name("btnK")
#検索のボタンはinputタグを取得する
search.click()
#click()を使うことでボタンを押すことができる。

これでヤフーニュースを検索する事ができた。取得したフォーム要素にclear()メソッドを使うと、フォームに入力した文字列を消去することができる。

現在表示されているウインドウ(ブラウザ)のURLを取得

seleniumはhtmlソースから要素を取得するだけでなく、htmlソース外からデータを取得することができる。具体的にはブラウザのurlである。 【Python】current_url・・・URLを取得する

current_urlメソッドを使うことでカレントページのURLを取得することができる。

current_url = driver.current_url

Chromewebdriverのヘッドレス化

通常運転するとseleniumchrome等のブラウザを立ち上げて、操作される。これではメモリを消費してしまう理由などからブラウザが見えない状態で操作される状態にすることができる。これは昔PhantomJSというwebdriverがその役割を担ってきたそうだが、メンテナンスをやめるということでPhantomJSからFirefoxChromeのヘッドレス化が一般になった模様だ。 ヘッドレス化はChromeのバージョンが59以降のものなら利用できる。バージョン確認方法はChrome://versionをブラウザに送ってあげると表示させることができる。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

CHROME_PASS = 'C:\\Users\\user\Desktop\chromedriver'

options = Options()
options.add_argument('--headless')
driver_headless = webdriver.Chrome(executable_path = CHROME_PASS, chrome_options=options)

#あとは普段どおりにseleniumのコードを書く

ポイントはwebdriverの定義でChromeに2種類の引数を渡すことだ。1つ目はChromedriverの実行ファイルのパスをkwargsとして渡す。2つ目はヘッドレスのオプションをkwargsとして渡すこと。

frame間の移動

iframeタグの中に存在する別のhtmlタグ以下の要素に対しては、そのまま要素を取得できない。そのため取得したい要素を含むiframeタグに移動し、そこから取得する流れに持ち込む。そのとき必要なのはswitch_to_frameやswitch_to_defaul_content()である。

3.4. Moving between windows and frames

driver.switch_to_frame("frameName")
#frameNameは要素の取得(find_element...)で定める。
driver.switch_to_default_content()
#親のフレームに戻る

別windowへ移動

aタグを押すことで別ウィンドウが開きそこの情報をスクレイピングしたいとする。webdriverはウィンドウが新たに開かれた場合にどのように挙動するのか?新たなウィンドウを開いたとて、現在のwebdriverのウィンドウを継続する。そのため新たに開いたウィンドウに移動する場合にはwebdriverのメソッド switch_to_window()を用いる必要がある。switch_to_window()の引数はwindowIDである。このwindowIDはwindow_handlesでリスト型のデータとしてwindowIDを取得することができる。

driver.get(URL)
window_ids = driver.window_handles
driver.swich_to_window(window_ids[-1]) 

https://stackoverflow.com/questions/13113954/selenium-webdriver-using-switch-to-windows-and-printing-the-title-doesnt-prin

python 3.7.1 selenium 3.14.1のバージョンだとswitch_to_window()が使えなかった。switch_to.window()として記述する必要がある。

classの名前が複数ある要素の取得について(有用)

例えばタグが以下のような状態の時、

input class=" first-part  copy-target"

このような場合には、いつもfind_element_by_class_nameを試みて、以下のようなエラーが返されていた。

selenium.common.exceptions.InvalidSelectorException: Message: invalid selector: Compound class names not permitted

Compound class names not permittedとは複数のクラスネームの場合は対応できませんよってことだと思う。これに対する解決方法がわかった。それはfind_element_by_css_selectorを使うことだ。上記の場合には、

driver.find_elements_by_css_selector(".first-part.copy-target")

参考URL Selenium Compound class names not permitted

VisualStudio Codeでエディタとターミナル間の移動をショートカットキーに登録する

参考

統合ターミナルウィンドウへフォーカスするショートカットキーを設定する - Qiita

Ctrl+;でエディタ-ターミナル間を移動する(VSCode)

前提

前提としてショートカットキーを登録するには、ショートカットキーを登録するファイルが存在するので、そのファイルに登録したいショートカットキーとその挙動を記述する、ということを知っておかなければならない。

ファイルの探し方とショートカットキーを登録するファイルの名前

ファイルの探し方

ctrl(command) + shift + F
または
ctrl(command) + shift + P 
のあとに
deleteキーを押して>マークを消す

ショートカットキーを登録するファイル名
ファイル名はkeybindings.jsonである。したがって コマンドパレットが開いたら、

#以下を打ち込む
keybindings.json

するとショートカットキーを登録するファイルが表示される。。

そのファイルに、

{
    "key": "ctrl+;",
    "command": "workbench.action.terminal.focus",
    "when": "editorTextFocus"
},
{
    "key": "ctrl+;",
    "command": "workbench.action.focusFirstEditorGroup",
    "when": "terminalFocus"
}

を[ ]内に貼り付ければいい。するとこんな感じになる。

// Place your key bindings in this file to override the defaults
[
    {
        "key": "ctrl+;",
        "command": "workbench.action.terminal.focus",
        "when": "editorTextFocus"
    },
    {
        "key": "ctrl+;",
        "command": "workbench.action.focusFirstEditorGroup",
        "when": "terminalFocus"
    } 
]

それでctrl+Sを押せばオッケイ。

ちなみにctrl + Shift + P からもkeyboardとかうってるとkeybindings.jsonにたどり着ける。

Djangoのpermissionを付与するサンプルコード

基本的なmodelに基づくpermissionは自動的に作成される。ということで、permisson自体は作らず、作成されているpermissonを付与するサンプルコードを書く。

permissionを付与するにはUserに直接付与するケースとGroupに付与し、そのGroupに属するUserが間接的にパーミッションをもたせるケースがある。

概要;

user1.user_permissions.set([permission])
group.permissions.set([permission])
user2.groups.set([group])

userにはuser_permissions関連オブジェクトが存在する。manyToMayなのでsetとかaddメソッドを使って、user_permissonsにpermissionをセットする事になる。

https://docs.djangoproject.com/ja/3.1/ref/contrib/auth/#django.contrib.auth.models.User.user_permissions


# Permissonオブジェクトを作成(migrate時に自動作成されていのでパス-> getで取得)
blog_view_permission = Permission.objects.get(name="""Can view blog""")

# perisson_userにBlogの読み取りアクセス権を与える
perisson_user.user_permissions.set([blog_view_permission])

Groupオブジェクトはpermissions関連オブジェクトが存在する。これはmanyToManyの関係なのでsetやaddメソッドでpermissonオブジェクトをセットする。で、Group自体はUser.groupsが関連オブジェクトなのでこれにもsetやaddでセットする。

https://docs.djangoproject.com/ja/3.1/ref/contrib/auth/#django.contrib.auth.models.User.groups  

https://docs.djangoproject.com/ja/3.1/ref/contrib/auth/#group-model

# Groupオブジェクトを作成
blog_view_group = Group.objects.create(name='blog_view_group')


# GroupオブジェクトにUserオブジェクトを紐づける
group_user = User.objects.get(username=group_user_data['username'])
group_user.groups.set([blog_view_group])


# 作成したGroupにPermissonオブジェクトを紐づける
blog_view_group.permissions.set([blog_view_permission])

gunicornについて

テーマ アプリケーション・サーバー間やりとり

PythonではアプリケーションとWebサーバのインタフェースとして PEP3333 で定義された WSGI(Web Server Gateway Interface)という仕様が広く利用されているらしくgunicornについて全く分かっていないので少し理解しておきたい。

gunicornやuWSGIはWSGIサーバと呼ばれる

WSGIサーバの種類 - gunicorn - uWSGI

gunicornはpythonで動いているものなのか??? 答えはyes リポジトリを見ればpythonで書かれていることを確認できる。

gunicornのリポジトリ https://github.com/benoitc/gunicorn/tree/5d0c7783008d4df87d579a576d19182c4a95caf7/gunicorn

ドキュメント:
https://docs.gunicorn.org/en/stable/run.html

$ gunicorn [OPTIONS] APP_MODULE

wsgiの仕様にあったpythonのコードであれば、WSGIサーバを動かすことができることがわかった。

  1. WSGIのアプリケーションは、2つの引数を持った呼び出し可能なオブジェクトである。
  2. 第2引数として渡されたオブジェクトを呼び出し、HTTPステータスコードとヘッダ情報を渡す。
  3. レスポンスボディとしてバイト文字列をyieldするiterableなオブジェクトを返す。

参考: https://c-bata.link/webframework-in-python/wsgi.html

wsgiの基準では第一引数は辞書型オブジェクトという決まりがあるらしい。 このオブジェクトにurlやHTTPメソッドを格納して利用することができる.

WSGIの説明で分かりやすいもの

では、WSGIとは何でしょうか?WSGI(ウィズギー)とは、PythonでWebアプリケーションとWebサーバーを接続する際に考案されたインターフェース定義です。 その昔、PythonのWebフレームワークが急増したことにより、Webアプリ開発者にある不都合が生じました。 フレームワークごとに、サーバーと接続するためのインターフェースが異なっていたため、同一のアプリケーションであっても、接続できるサーバーが制限される(あるサーバーには接続できるのに、別のサーバーには接続できない)という事態が生じてしまったのです。 そのため、インターフェースを統一して、どのフレームワークでも同じように各種サーバーに接続できるようにしよう、ということで生まれたのがWSGIでした。 WSGIアプリケーションの定義として、 WSGIアプリケーションは、呼び出し可能なオブジェクトとして定義する。このオブジェクトが呼び出される際、第一引数に環境変数が渡され、第二引数にステータスコードとレスポンスヘッダを受け取る呼び出し可能なオブジェクトが渡される。 第二引数に渡されたオブジェクトを呼び出して、ステータスコードとレスポンスヘッダ情報を渡す。 戻り値として、バイト文字列をyieldするiterableなオブジェクトを返す。 といったものがあります。

https://qiita.com/sti320a/items/828d7bceabea5f363ad1

# ファイル名はhello.py

def application(env, start_response):
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']

gunicornを起動(gunicorn -w 1 hello:application)してlocalhost:8000にアクセスすると、ブラウザにHello Worldが表示される。

以下の場合も試してみた。

def application(change1, change2):
    print('change1 :,'change1)
    print('change2',change2)
    change2('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']

すると同じように動いた。gunicornではひいてはwsgi仕様では、第一引数と第二引数があり、それらを使って関数さえ書いていればブラウザに表示できることがわかった。
change1, change2を標準出力した結果を残しておく。

change1:  {'wsgi.errors': <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x10ff16460>, 'wsgi.version': (1, 0), 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': <class 'gunicorn.http.wsgi.FileWrapper'>, 'wsgi.input_terminated': True, 'SERVER_SOFTWARE': 'gunicorn/20.0.4', 'wsgi.input': <gunicorn.http.body.Body object at 0x10ff165b0>, 'gunicorn.socket': <socket.socket fd=9, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000), raddr=('127.0.0.1', 50644)>, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'RAW_URI': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:8000', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_CACHE_CONTROL': 'max-age=0', 'HTTP_DNT': '1', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'HTTP_SEC_FETCH_SITE': 'none', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_SEC_FETCH_USER': '?1', 'HTTP_SEC_FETCH_DEST': 'document', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'ja-JP,ja;q=0.9,es-ES;q=0.8,es;q=0.7,en-US;q=0.6,en;q=0.5', 'HTTP_COOKIE': 'csrftoken=LD89SVbaQvSe6eBpG8yp7zseGHi1yJpaFephs4Bi4SuAgkNFutL027VJmgasezv6; sessionid=n77unsdykcmss7i86rjb7rpziegzro79', 'wsgi.url_scheme': 'http', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '50644', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'PATH_INFO': '/', 'SCRIPT_NAME': ''}
start_response <bound method Response.start_response of <gunicorn.http.wsgi.Response object at 0x10ff16490>>
change1 :  {'wsgi.errors': <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x10ff16550>, 'wsgi.version': (1, 0), 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'wsgi.file_wrapper': <class 'gunicorn.http.wsgi.FileWrapper'>, 'wsgi.input_terminated': True, 'SERVER_SOFTWARE': 'gunicorn/20.0.4', 'wsgi.input': <gunicorn.http.body.Body object at 0x10ff16490>, 'gunicorn.socket': <socket.socket fd=9, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8000), raddr=('127.0.0.1', 50645)>, 'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'RAW_URI': '/favicon.ico', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:8000', 'HTTP_CONNECTION': 'keep-alive', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36', 'HTTP_DNT': '1', 'HTTP_ACCEPT': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8', 'HTTP_SEC_FETCH_SITE': 'same-origin', 'HTTP_SEC_FETCH_MODE': 'no-cors', 'HTTP_SEC_FETCH_DEST': 'image', 'HTTP_REFERER': 'http://localhost:8000/', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_ACCEPT_LANGUAGE': 'ja-JP,ja;q=0.9,es-ES;q=0.8,es;q=0.7,en-US;q=0.6,en;q=0.5', 'HTTP_COOKIE': 'csrftoken=LD89SVbaQvSe6eBpG8yp7zseGHi1yJpaFephs4Bi4SuAgkNFutL027VJmgasezv6; sessionid=n77unsdykcmss7i86rjb7rpziegzro79', 'wsgi.url_scheme': 'http', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': '50645', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'PATH_INFO': '/favicon.ico', 'SCRIPT_NAME': ''}
change2 <bound method Response.start_response of <gunicorn.http.wsgi.Response object at 0x10ff165e0>>

よく分からないけど二回出力されていることを確認した。

第一引数には何が格納されているか分かりづらいので以下のようにして出力をみた。

def application(env, start_response):
    print('env : ', env)
    print('env.keys() : ', env.keys())
    print('start_response', start_response)
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    return [b'Hello World']
env.keys() :  dict_keys(['wsgi.errors', 'wsgi.version', 'wsgi.multithread', 'wsgi.multiprocess', 'wsgi.run_once', 'wsgi.file_wrapper', 'wsgi.input_terminated', 'SERVER_SOFTWARE', 'wsgi.input', 'gunicorn.socket', 'REQUEST_METHOD', 'QUERY_STRING', 'RAW_URI', 'SERVER_PROTOCOL', 'HTTP_HOST', 'HTTP_CONNECTION', 'HTTP_USER_AGENT', 'HTTP_DNT', 'HTTP_ACCEPT', 'HTTP_SEC_FETCH_SITE', 'HTTP_SEC_FETCH_MODE', 'HTTP_SEC_FETCH_DEST', 'HTTP_REFERER', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_COOKIE', 'wsgi.url_scheme', 'REMOTE_ADDR', 'REMOTE_PORT', 'SERVER_NAME', 'SERVER_PORT', 'PATH_INFO', 'SCRIPT_NAME'])

次にこれで実行してみた。なお、'http://localhost:8000/sjfof'をブラウザに入力している。

def application(env, start_response):
    #print('env : ', env)
    #print('env.keys() : ', env.keys())
    #print('start_response', start_response)
    print('================start_response前 始まり========================')
    print('env["REQUEST_METHOD"] : ', env["REQUEST_METHOD"])
    print('env["QUERY_STRING"] : ', env["QUERY_STRING"])
    print('env["RAW_URI"] : ', env["RAW_URI"])
    print('env["HTTP_HOST"] : ', env["HTTP_HOST"])
    print('================start_response前 終わり========================')
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    print('================start_response後 始まり========================')
    print('env["REQUEST_METHOD"] : ', env["REQUEST_METHOD"])
    print('env["QUERY_STRING"] : ', env["QUERY_STRING"])
    print('env["RAW_URI"] : ', env["RAW_URI"])
    print('env["HTTP_HOST"] : ', env["HTTP_HOST"])
    print('================start_response後 終わり========================')
    return [b'Hello World']
================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /sjfof
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /sjfof
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================
================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================

'http://localhost:8000/sjfof?mykey=myvalue&second=value2'をブラウザに入力している。

================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  mykey=myvalue&second=value2
env["RAW_URI"] :  /sjfof?mykey=myvalue&second=value2
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  mykey=myvalue&second=value2
env["RAW_URI"] :  /sjfof?mykey=myvalue&second=value2
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================
================start_response前 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response前 終わり========================
================start_response後 始まり========================
env["REQUEST_METHOD"] :  GET
env["QUERY_STRING"] :  
env["RAW_URI"] :  /favicon.ico
env["HTTP_HOST"] :  localhost:8000
================start_response後 終わり========================

この結果からどうやら二回標準出力は2回繰り返される。そして第一引数この場合はenvは1回目と2回目で内容が変化することだけわかった. それかfaviconuriにアクセスするために二種類のリクエスト情報が作られたと考える方が正しいかもしれない。これは進めていけばいずれわかることだろう。

ここまでわかったこと(後日分かったことも追加している)

第一引数キー
REQUEST_METHOD GET、POST等のリクエストメソッド
QUERY_STRING ?以降の内容
RAW_URI ブラウザに入力したURIもしくはkeep aliveで追加でアクセスするURIな気がする
HTTP_HOST 接続先のホスト
PATH_INFO RAW_URIからQUERY_STRING部分を削除した、いわゆるPATH値が格納される

また、ブラウザに異なるURIを入力してもenv['RAW_URI']にURIが格納されるだけで、実際に起動している 関数(この場合はhello.py application関数)が起動するだけである。したがってapplication関数内でRAW_URIの内容によって表示する条件分岐を作ればリソースのアドレス可能性を担保できるようになるのではないか??

APIViewのpermission_classesはどんな仕組みで動いているのか

APIViewのプロパティは以下の通り。

# APIViewのプロパティ  

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES #コイツ
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

    # Allow dependency injection of other settings to make testing easier.
    settings = api_settings

    schema = DefaultSchema()

次にpermission_classesを使っているメソッドは何があるか??
それはget_permissionsだった。このメソッドはpermisson_classをインスタンス化したリストを作成する役割である。

# https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/views.py#L274

    def get_permissions(self):
        """
        Instantiates and returns the list of permissions that this view requires.
        """
        return [permission() for permission in self.permission_classes]

じゃあ、このget_permissionsメソッドはどの文脈で使われているかということが次の疑問になる。
それは以下の通りcheck_permissionsとcheck_object_permissionsメソッドで使われていることが判明した。

    def check_permissions(self, request):
        """
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

    def check_object_permissions(self, request, obj):
        """
        Check if the request should be permitted for a given object.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_object_permission(request, self, obj):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

こレラのメソッドはパーミッションがない場合にパーミッション拒否のメソッドが実行される。そういう意味で、permission.has_permission()メソッドがパーミッションのチェックとなっている。 このメソッドはpermissons.pyの各クラスメソッドに規定されている。

https://github.com/encode/django-rest-framework/blob/d635bc9c71b6cf84d137a68610ae2e628f8b62b3/rest_framework/permissions.py